文章指出 AI 功能中 prompt 本身不能作为安全控制手段,需在应用层建立确定性执行边界来验证强制审批;并提出跨信任边界的数据流需逐条标注属性。
如果某个操作需要审批,提示词永远不是 enforcement(强制执行)的控制手段。
提示词可以告诉模型问:"我应该关闭这个工单吗?"这有助于对话。但它不能保证模型真的会问,不能保证用户看到的是确切的操作,也不能保证确认后参数保持不变。必须有一个确定性的执行边界来在执行前验证强制审批。
我对其他 AI 输出也使用相同的规则:模型可以提出建议。应用程序决定该建议是否成为行为。
信任边界是权威、安全域、数据暴露或既定信任属性发生变化的边界。有些边界保护操作,有些边界保护数据在传输到模型提供商、日志接收点、浏览器、插件或其他租户时的安全。
信任取决于你需要的属性
"可信"和"不可信"单独看太过笼统。
经过身份验证的用户有已验证的身份,但他们的文本仍然是不可信的输入。内部文档可能有已知的来源,但可能已过时或超出了当前用户的权限。模型输出可能符合 JSON schema,但仍然可能请求调用者无法执行的操作。
对于跨边界的每个值,需要明确已建立哪些属性:
身份和委托:谁发起、批准并执行请求,每个步骤在谁的授权下运行?
授权:每个身份可以访问或修改什么?
来源和完整性:数据来自哪里,是否可能在预期流程之外被改变?
新鲜度和状态:数据是否足够新,目标是否仍然满足预期条件?
有效性:值是否满足语法和域规则?
审批:授权审查者是否批准了这个确切的操作?
这些属性不能相互替代。身份验证不能让输入变得安全。Schema 验证不能授予访问权限。审批也不能冻结权限或资源状态。
追踪整个功能链路
以一个支持助手为例,它可以搜索客户记录、摘要工单、起草回复,并请求关闭工单。
操作路径在策略分类后分支:
authenticated support user
-> application resolves identity and tenant
-> trusted data-access path constrains retrieval
-> selected content crosses to the model provider
-> model proposes a response or action
-> application parses and validates the proposal
-> application loads the target inside the caller's tenant
-> authorization evaluates caller and resource
-> application policy classifies the canonical action
Deny
-> stop and record the decision
Allow
-> execution boundary re-checks current state, authorization, and policy
-> executor atomically persists the action and Allow decision while claiming a durable Executing attempt
RequireApproval
-> approval service stores the canonical operation
-> authorized reviewer approves or rejects that operation
-> execution boundary re-checks current state, authorization, and policy
-> executor atomically moves Approved to Executing and creates a durable attempt
-> executor performs the effect
-> executor records the execution outcome
-> application applies destination-specific output handling
日志和追踪跨越这条路径的多个部分。它们可以捕获提示词、检索的文档、工具参数、资源标识符、审批细节和模型响应。将遥测视为独立的数据边界。决定哪些字段可以离开应用程序,屏蔽敏感值,并避免将完整的提示词捕获作为默认行为。
对模型提供商的调用是另一个边界。只发送任务所需的数据。应用程序策略首先根据请求、数据分类和安全上下文确定允许的路由集合。然后模型可以在该集合内选择非安全敏感的选项,如快速模式或推理模式。模型输出绝不能选择任意的提供商、部署、区域、保留设置或凭证。
模型参与流程,但不拥有这些安全决策。同样的设计适用于聊天 UI、AI 智能体框架、后台 worker 或自定义编排代码。
绘制改变风险的边界
我最关注数据获得授权的地方,因为那是看似合理的字符串变成真实副作用的地方。当机密信息跨越到信任度较低的域时,数据暴露同样重要。
我不会把任何一行标记为"提示词注入防御"。没有任何过滤器可以证明检索的文本是无害的。有用的边界限制该文本可以影响什么,并在模型处理后验证每个特权转换。
不要把安全上下文放在模型参数中
模型可以提出操作。当操作需要目标数据(如身份或范围)时,例如请求查看某个命名租户的配置,模型也可以提供这些。但模型绝不能提供或覆盖操作运行时的已验证身份、租户范围、委托授权或授权结果。
不要暴露这样的工具契约:
Task CloseTicketAsync(
string tenantId,
string userId,
string ticketId,
bool userIsAuthorized);
模型控制每个参数。函数无法区分真正的授权结果和一个看起来很真的布尔值。
对受信任的值使用经过身份验证的应用程序上下文。在策略评估之前先将提案规范化为规范操作:
public enum ActionDisposition
{
Allow,
Deny,
RequireApproval
}
public sealed record CloseTicketProposal(
string? TicketId,
string? ResolutionSummary);
public sealed record CloseTicketArguments(
string ResolutionSummary);
public sealed record ProposedAction(
string Operation,
string RequesterId,
string TenantId,
string Environment,
string ResourceId,
long ResourceVersion,
CloseTicketArguments Arguments);
public sealed record ApprovalRequirement(
string ReviewerPolicy,
string RiskClass,
bool AllowSelfApproval,
TimeSpan ValidFor);
public sealed record ActionDecision
{
private ActionDecision(
ActionDisposition disposition,
ProposedAction? action,
ApprovalRequirement? approval,
string decisionSourceId,
string? policyVersionId,
string? reason)
{
Disposition = disposition;
Action = action;
Approval = approval;
DecisionSourceId = decisionSourceId;
PolicyVersionId = policyVersionId;
Reason = reason;
}
public ActionDisposition Disposition { get; }
public ProposedAction? Action { get; }
public ApprovalRequirement? Approval { get; }
public string DecisionSourceId { get; }
public string? PolicyVersionId { get; }
public string? Reason { get; }
public static ActionDecision Allow(
ProposedAction action,
string decisionSourceId,
string? policyVersionId = null) =>
new(
ActionDisposition.Allow,
action,
null,
decisionSourceId,
policyVersionId,
null);
public static ActionDecision Deny(
ProposedAction? action,
string reason,
string decisionSourceId,
string? policyVersionId = null) =>
new(
ActionDisposition.Deny,
action,
null,
decisionSourceId,
policyVersionId,
reason);
public static ActionDecision RequireApproval(
ProposedAction action,
ApprovalRequirement approval,
string reason,
string decisionSourceId,
string? policyVersionId = null) =>
new(
ActionDisposition.RequireApproval,
action,
approval,
decisionSourceId,
policyVersionId,
reason);
}
边界通过租户范围的路径加载工单并检查资源授权。这里的摘要验证有意做得很小。实际的支持系统可能会限制格式、要求填写解决代码,或在存储前检查敏感数据。审查员 UI 仍然必须将摘要编码为不可信文本。
public sealed class TicketActionBoundary(
TicketStore tickets,
CurrentRequest currentRequest,
IAuthorizationService authorization,
TicketActionPolicy policy)
{
private const string DecisionSourceId = "ticket-action-boundary:v1";
public async Task<ActionDecision> EvaluateAsync(
CloseTicketProposal proposal,
CancellationToken cancellationToken)
{
string summary = proposal.ResolutionSummary?.Trim() ?? string.Empty;
```csharp
if (string.IsNullOrWhiteSpace(proposal.TicketId) ||
summary.Length is 0 or > 500)
{
return ActionDecision.Deny(
null,
"Invalid proposal.",
DecisionSourceId);
}
SupportTicket? ticket = await tickets.FindAsync(
currentRequest.TenantId,
proposal.TicketId,
cancellationToken);
if (ticket is null)
{
return ActionDecision.Deny(
null,
"Ticket not found.",
DecisionSourceId);
}
AuthorizationResult result = await authorization.AuthorizeAsync(
currentRequest.User,
ticket,
"CloseSupportTicket");
if (!result.Succeeded)
{
return ActionDecision.Deny(
null,
"Action is not allowed.",
DecisionSourceId);
}
var action = new ProposedAction(
Operation: "support-ticket.close",
RequesterId: currentRequest.UserId,
TenantId: currentRequest.TenantId,
Environment: currentRequest.Environment,
ResourceId: ticket.Id,
ResourceVersion: ticket.Version,
Arguments: new CloseTicketArguments(summary));
return policy.Classify(ticket, action);
}
}
ASP.NET Core 通过 IAuthorizationService.AuthorizeAsync(user, resource, policyName) 支持这种基于资源的检查。评估策略的授权处理器会收到已认证的 principal 和已加载的工单作为资源。该模型既看不到授权标志位,也看不到租户选择器。
ProposedAction 是操作的应用程序级规范表示。它包含应用程序所有的安全上下文和执行前提条件。在规范化之前的拒绝没有操作对象。一旦操作被规范化,策略拒绝会保留它,以便审计记录显示被评估和被拒绝的内容。
每个确定性决策点都会附加自己的稳定源 ID。TicketActionBoundary 标识验证和授权拒绝。TicketActionPolicy 标识分类决策,并单独记录其策略版本。调用方无法在决策发生之前提供过时的策略版本。
在这个示例中,CurrentRequest.UserId 和 GetUserId() 返回相同的应用程序级规范用户 ID。如果应用程序直接比较外部身份,必须将发行方和相关租户或安全域与主体值一起包含。
强制审批始终属于应用程序策略或其他确定性执行边界。提示可以要求确认以使交互更清晰。但对话中的答案不是审批产物,不能绕过策略。
该策略至少需要三种结果:
public sealed class TicketActionPolicy
{
public const string DecisionSourceId = "ticket-action-policy";
public const string PolicyVersionId = "support-ticket-close:v3";
public ActionDecision Classify(
SupportTicket ticket,
ProposedAction action)
{
if (ticket.Status is not TicketStatus.Resolved)
{
return ActionDecision.Deny(
action,
"Only resolved tickets can be closed.",
DecisionSourceId,
PolicyVersionId);
}
if (ticket.IsEscalated ||
ticket.Priority is TicketPriority.Critical)
{
return ActionDecision.RequireApproval(
action,
new ApprovalRequirement(
ReviewerPolicy: "ApproveSupportTicketClosure",
RiskClass: "high",
AllowSelfApproval: false,
ValidFor: TimeSpan.FromMinutes(15)),
"Escalated or critical tickets require review.",
DecisionSourceId,
PolicyVersionId);
}
return ActionDecision.Allow(
action,
DecisionSourceId,
PolicyVersionId);
}
}
本例允许授权用户自动关闭普通的已解决工单。它要求对紧急或关键工单进行审核,并在工作未解决时拒绝关闭。
真实策略还可以考虑环境、目标、金额、数量、批量范围、可逆性和合规影响。只读并不意味着低风险。导出每条客户记录或检索密钥可能比可逆写操作更需要严格策略。
不要仅仅因为模型选择了工具就要求审批。当操作的披露性、外部性、财务性、破坏性、权限性或合规影响超过应用程序的自动执行策略时,才要求审批。
如果对影响性操作的风险分类或策略查找失败,则停止。不要回退到自动执行。
审批服务应该持久化规范的 ProposedAction、策略决策和 ApprovalRequirement。另一个选项是持久化一个带一致性摘要的规范表示。审批客户端收到审批 ID 和显示数据。它不会重建操作以供执行。
规范序列化使摘要可重现。但它不会使普通加密哈希具有防篡改性。攻击者如果能修改操作及其摘要,就可以简单地重新计算哈希。只将纯哈希用于在已受保护的存储内检测意外不一致。
如果值必须在信任边界间保护完整性,请使用带有受保护服务器端密钥的 HMAC、数字签名或存储在单独受保护信任域中的摘要。在服务器上从一种文档化的序列化格式计算它。属性顺序、数字和时间戳格式、Unicode 规范化、空处理和集合顺序必须是确定性的。
一条有用的审批记录包括:
审核者也需要授权。在接受决策之前,检查该审核者是否可以为相应租户、环境、资源和风险类别审批此操作。对于某些操作,请求者不得审批自己的提案。
public sealed record ApprovalAuthorizationContext(
ProposedAction Action,
ApprovalRequirement Requirement);
var approvalContext = new ApprovalAuthorizationContext(
pendingApproval.ProposedAction,
pendingApproval.Requirement);
AuthorizationResult reviewerAccess = await authorization.AuthorizeAsync(
reviewer,
approvalContext,
pendingApproval.Requirement.ReviewerPolicy);
if (!reviewerAccess.Succeeded ||
(!pendingApproval.Requirement.AllowSelfApproval &&
pendingApproval.ProposedAction.RequesterId == reviewer.GetUserId()))
{
return ApprovalResult.Denied("Reviewer is not authorized.");
}
return await approvals.RecordDecisionAsync(
pendingApproval.ApprovalId,
reviewer.GetUserId(),
approved,
cancellationToken);
审核者授权可能依赖于规范操作中不包含的当前资源属性,例如所有权、业务单元或分类。通过受信任的资源路径加载这些属性,然后在评估审核者策略之前进行。将生成的应用程序定义上下文传递给 AuthorizeAsync。不要要求审核者客户端或模型提供它。
RecordDecisionAsync 必须原子地将记录从 Pending 移动到 Approved 或 Rejected。它拒绝过期的、已决策的或其他无效的记录。使用事务、行版本、比较并交换更新或等效的并发机制,使两个审核者不能覆盖彼此的决策。客户端或控制器中的检查是不够的。
未经授权的提交会被拒绝。它不是拒绝决策。将 Rejected 保留给明确选择不批准操作的授权审核者。
审批 UI 是一个信任边界。将模型生成的摘要渲染为不可信文本,并将其与应用程序加载的字段(如请求者、工单 ID、当前优先级、环境和预期效果)分开。生成的标记不得冒充权威 UI 或隐藏操作的部分内容。
审批绑定到规范操作,而不是解释的说服力。
对于使用环境凭证(如认证 Cookie)的浏览器端审批端点,应采用与其他状态变更端点相同的请求完整性和防伪造保护。
重新检查一切可能变更的内容
自动 Allow 与已审批操作以不同的准入证据进入同一个执行边界。两条路径都不会冻结授权、策略或资源状态。
两条路径应按以下方式汇合:
自动允许的操作
-> 验证 Allow 决策和规范操作
-> 通过可信租户路径重新加载资源
-> 重新授权请求方
-> 比较资源版本和预期状态
-> 评估当前操作策略
-> 原子性地持久化操作和当前 Allow 决策,同时声明一个持久的 Executing 尝试
已审批的操作
-> 验证绑定、过期时间、决策时审核员的授权证据和决策状态
-> 通过可信租户路径重新加载资源
-> 重新授权每个当前依赖其权限执行的标识
-> 比较资源版本和预期状态
-> 评估当前操作策略和审批兼容性
-> 原子性地将 Approved 转为 Executing 并创建持久尝试
两条路径
-> 执行效果时需满足幂等性或显式重复处理
-> 将 Executing 转为 Completed、OutcomeUnknown、FailedRetryable 或 FailedFinal
-> 记录结果,但不泄露敏感内容
如果资源变更可能导致执行失效,应将声明条件设定为刚刚检查过的版本。乐观并发条件是一种选择。另一种是在提交点之前阻止不兼容的变更。对于外部效果,需定义该点并决定如何协调冲突的状态变更。
持久尝试和幂等性规则也适用于需要它们的自动允许操作。审批改变的是准入路径,而非执行的可靠性要求。
自动路径不需要单独的持久 Allowed 状态。其执行声明将规范操作和当前 Allow 决策作为准入证据与持久尝试一起原子性地持久化。
在审批路径上,审核员授权在决策时是强制的。在执行期间是否重新验证审核员取决于策略。某些系统将审批视为审核员角色变更后的有效历史行为。其他人则要求该权限在执行前保持不变。记录系统使用的规则。
自动路径在重新评估策略时仍必须收到 Allow。在审批路径上,当前的 Deny 会停止执行。变更的审批要求会使存储的审批失效,除非当前策略明确接受在其记录版本下创建的审批。后来的 Allow 不得静默移除存储决策所需的控制。
事务设计取决于副作用。数据库更新通常可以在一个事务中移动执行状态并应用变更,同时使用乐观并发令牌。外部邮件或支付无法共享该事务。
对于外部效果,当提供者支持时,使用持久执行记录或发件箱以及幂等性密钥。恢复时必须将放弃或过期的 Executing 尝试视为可能已应用。在重试前协调外部结果或依赖安全的幂等性机制。跨本地数据库和任意外部系统不存在通用的 exactly-once 保证。
审批状态转换和幂等性解决的是不同问题。状态机阻止审批决策或执行声明被重放。幂等性防止同一操作的重复效果。资源版本检查检测资源版本策略覆盖范围内的变更。显式预期状态检查决定安全或业务相关的先决条件是否仍然成立。
在启动效果之前,重新授权、当前状态加载、策略评估、持久尝试持久化或所需审批验证中的任何失败都必须阻止执行。在外部效果可能已发生后,持久化失败不能将该效果转为拒绝。如有可能,将尝试转为 OutcomeUnknown,阻止盲目重试,并协调外部结果而非将操作标记为 Pending 或 Rejected。
结构化输出收窄接口边界
结构化输出使边界更容易检查。它可以在业务逻辑运行之前拒绝缺失的属性、错误类型和不支持的形状。
它无法建立意图或权限。以下是有效的 JSON:
{
"ticketId": "ticket-from-another-tenant",
"resolutionSummary": "Customer confirmed resolution."
}
不要询问模型该工单是否属于当前租户。通过可信租户路径加载它,并在应用代码中评估授权。
不同的模型控制值需要不同的控制。优先使用预定义查询或查询构建器,而非任意生成的 SQL。
对于 URL,限制协议和目标。用于连接的网络端点必须满足 DNS/IP 策略。不要验证一次解析然后允许 HTTP 客户端执行第二次未检查的解析。为每个重定向目标禁用重定向或重复目标及 DNS/IP 检查。HttpClientHandler.AllowAutoRedirect 默认为 true,因此对于服务端抓取要主动配置它。
针对允许的根目录解析文件路径,并处理遍历和链接。将目标和数据丢失策略应用于收件人地址。将配置选择映射到允许列表中的标识符。
不存在通用的 ValidateModelOutput() 调用能使所有这些值变得安全。
检索的内容和工具结果仍是数据
在调用方或模型能够观察受保护内容或结果元数据之前,在可信检索或数据访问路径内强制执行租户和资源授权。元数据过滤器是一种选择。行级安全性