一次退款请求为什么能改写生产数据

工位上的工程师收到一张客服退款工单,订单金额是149美元,用户却要求退回10000美元。新接入的AI Agent会读邮件、生成查询语句,还能直接调用生产数据库。监控里出现了一条UPDATE记录,WHERE条件指向了另一个账户。这个瞬间说明,能写代码的助手已经开始改变真实业务状态。工单系统记录的只是请求,真正要保护的是落库动作。

恶意请求往往不需要复杂技巧,用户只要把破损、紧急和超额退款写在同一段话里,模型就可能把它们拼成一个看似合理的动作。它会先读取订单,再计算金额,接着调用退款工具。每一步单独看都像正常流程,串起来却可能越过审批边界。真正危险的地方不是模型会回答,而是回答会触发副作用。

这类事故也不局限于退款,修改库存、删除客户、发起付款和执行脚本都属于同一种风险。Agent一旦拿到共享数据库连接,攻击者就能借它的权限改变记录。日志可能只留下模型说了什么,却没有留下谁批准了什么。没有可验证的身份,事后排查只能依赖猜测。资产范围也应随请求一起确认。

Google Developers Blog在2026年8月17日讨论了这类零信任Agent方案,场景正是客服与退货流程。文章把风险拆成数据库写入、动态代码执行和模型输入输出三个位置。它的重点不是给系统提示词增加更多禁令,而是把约束放到模型之外。对工程团队来说,这个思路比换一个更听话的模型更有落地价值。

本文沿着一次退款请求的路径来拆解这套做法,先看谁有权写入,再看代码能跑到哪里,最后看业务规则在哪里拦截。你会看到Cloud KMS、HSM和gVisor分别解决什么问题。你也会得到一段只依赖Python标准库的本地验证代码。代码能帮助你理解流程,但生产环境仍要接入真实的密钥、沙箱和审批系统。

零信任不是把Agent当成敌人,而是承认它可能被输入带偏。模型可以负责理解自然语言和选择工具,却不能单独决定一笔不可逆的写入。每个副作用动作都应留下可验证的证据,并经过独立规则检查。这样即使模型判断失误,错误也会在真正落库前停住。模型仍能规划,但不再拥有最后一笔写入权。

先把动作分成读取和写入,安全边界才有清楚的落点。读取可以自动完成,写入必须带着资源和审批信息。金额、订单状态与请求人都要成为结构化字段。这样的划分让工程师能在每个副作用发生前插入拒绝点。这一步也让测试环境与生产环境可以使用同一套接口。规则可核对。

系统提示词挡不住越权

很多项目的第一道防线是一句“退款金额不得超过订单金额”,再配上一段很长的系统提示词。它在正常对话里看起来有效,因为模型会复述规则并给出礼貌答复。可是提示词只是上下文中的文字,不是数据库能够验证的权限。攻击者改变输入,模型就可能重新解释那条规则。

模型还会受到上下文混淆的影响,客服邮件、网页内容和工具返回值都可能携带新的指令。只要这些内容进入同一个上下文,模型就要在不同可信等级之间做判断。它没有硬件身份,也没有天然的事务边界。把安全责任全部压给它,等于让执行者自己决定自己是否违规。

传统应用通常把权限写在服务账号、网关和数据库策略里,Agent系统却常把权限藏在自然语言里。自然语言适合描述意图,不适合证明授权。一个“请帮我处理退款”的句子不能说明金额上限、订单归属和审批人。真正的授权必须落在结构化字段和可验证的策略上。授权字段必须由服务端生成和核对。

可以把一次工具调用拆成四个问题:谁发起,改了什么,为什么能改,以及谁批准。模型只能提供候选答案,不能自己回答完全部问题。签名负责确认操作者,网关负责检查业务条件,数据库入口负责最后拒绝。三者各自独立,任何一层失败都不应由模型自行绕过。每个答案都要可追溯。

边界解决的问题失效时的后果
签名确认哪个Agent提交了写入无法证明来源或发现篡改
沙箱限制动态代码接触主机可能读密钥或连接外网
语义网关检查业务规则和数据流向合法身份也可能执行错事

表里的三道边界不是并列的产品开关,而是一条连续的拒绝链。签名通过并不代表业务允许,沙箱隔离也不代表数据正确。每一层都应该能够独立记录拒绝原因,并把结果传给审计系统。这样排查时能知道请求究竟在哪一扇门前被挡住。拒绝结果不能被后续模型调用覆盖。

安全设计还要把“读”和“写”分开,读取订单通常可以自动完成,退款和删除则必须提高门槛。权限模型不应只按工具名称区分,还要看参数、资源归属和金额。相同的refund工具,处理测试订单与处理生产订单,风险完全不同。模型看到的是一句话,策略看到的应是一组字段。策略越靠近资源,越容易在事故前生效。

给每一次写入绑定可验证的身份

第一道硬边界放在数据库入口之前,Agent不能把一条裸SQL直接交给生产系统。它需要先构造一个待签名载荷,里面写明Agent身份、动作类型、资源编号和变更值。签名覆盖的是完整载荷,而不是一段描述性文字。任何字段在传输中被改动,验签都应该失败。写入服务还要拒绝缺失字段和异常格式。

生产环境可以为每个Agent分配独立的Service Account,再给它绑定Cloud KMS里的非对称签名密钥。私钥由HSM保护,应用拿到的是受权限控制的签名能力,不是可复制的密钥文件。这样容器被读取时,攻击者也很难直接带走私钥。每个Agent的权限范围和审计记录也能单独管理。密钥权限变化也要进入审计记录。

签名之前必须固定序列化规则,否则同一份数据在不同语言或不同字段顺序下会得到不同摘要。常见做法是对JSON对象按键排序,并明确字符编码和分隔符。金额也要使用统一的整数分单位,避免浮点数在签名和验签时出现差异。安全协议最怕的不是复杂,而是两端理解不一样。

数据库服务收到载荷后,不能只相信调用方附带的Agent名称。入口服务应根据密钥目录找到对应公钥,验证签名,再检查资源归属和业务阈值。通过以后才开启事务,并把原始载荷、签名和结果写入不可随意修改的审计记录。验签失败要在写入发生前返回,不能先写后补日志。

Google的本地演示用HMAC模拟Cloud KMS,原因是HMAC可以在没有云账号的电脑上跑通签名和验签。它不能替代HSM,也不能提供同样的密钥隔离能力。工程师可以先用它验证字段覆盖、篡改检测和失败路径。迁移到生产时,再把签名实现换成Cloud KMS客户端。这样本地测试也能看到失败路径,并验证篡改确实会被拒绝。

这里有一个常被忽视的细节:签名证明的是“这个载荷来自某个Agent”,并不证明“这个载荷符合业务”。一个拥有合法密钥的Agent仍可能因为提示注入而提交超额退款。于是数据库入口还要重复检查金额、订单状态和审批标记。身份验证与业务授权必须同时成立,不能互相替代。签名是身份证明,不是业务通行证。

审计记录也要能回答时间问题,载荷中应带请求编号和单调可比较的时间字段。服务端要拒绝过期请求、重复请求和已经消费过的请求。否则攻击者拿到一条旧的合法签名,就可能再次提交同一笔操作。签名让篡改容易被发现,重放保护让旧请求无法无限复用。请求状态也要有明确的生命周期。

让动态代码待在没有网络的沙箱里

Agent常被赋予一个run_python工具,用来处理附件、计算价格或转换数据。这个工具一旦直接调用主进程的exec或subprocess,模型生成的代码就进入了应用所在的运行环境。代码可能读取环境变量、扫描文件,也可能打开网络连接。它不需要理解业务,只要找到一条可利用的路径就够了。它的权限越大,潜在损失就越难收拾。

普通容器并不是完整的安全边界,因为容器进程仍然共享宿主机内核。配置错误、过大的Linux能力或内核漏洞都可能让隔离失效。即使不考虑逃逸,容器里默认开放的网络和文件权限也足以造成数据外传。动态代码执行应被当成高风险外部输入,而不是普通函数调用。所以不能只看镜像名称。

gVisor提供了一层用户态内核,常见实现会由Sentry截获系统调用,再交给受控的运行时处理。它不是把不可信代码变成可信代码,而是减少代码能观察和触碰的对象。Agent只应该拿到完成任务所需的最小目录和最小输入。其余文件、设备和主机能力都应从沙箱中消失。关键是减少暴露面,而不是改变模型行为。

网络隔离是这道边界的核心,运行不可信代码时可以明确设置network为none。没有默认路由、DNS和外部接口,代码即使尝试发送环境变量也没有出口。文件挂载应使用只读模式,容器能力应全部丢弃。资源限制则负责防止死循环和内存消耗拖垮宿主机。这些限制都要在启动参数中显式写出。

沙箱配置还要设定CPU、内存、临时目录大小和最长执行时间,所有上限都应写入部署文件。超时后要销毁运行实例,而不是继续等待模型给出解释。返回结果也要限制长度并清理控制字符,避免把终端内容原样送回上下文。隔离的目标是把失败关在一次任务里。上限应按任务类型分别设定。

在本地验证时,可以故意让测试代码读取环境变量并创建socket连接,再观察沙箱返回的错误。这个测试不能只看进程是否退出,还要检查宿主机没有新增文件、网络连接和异常权限。对于生产集群,还要把逃逸测试、资源耗尽测试纳入发布前验收。安全边界必须用失败案例证明,而不是用配置截图证明。

gVisor也不是万能药,宿主机内核、容器运行时和挂载策略仍然需要维护。沙箱内若放入长期有效的云凭证,攻击者依旧可能在允许的范围内滥用权限。更稳妥的办法是使用短时令牌、无网络执行和专门的结果服务。沙箱负责限制运行环境,凭证治理负责限制数据价值。云端凭证仍然要按最小权限配置。

在模型输入输出外再加一层语义网关

第三道边界处理的是签名和沙箱都无法判断的业务语义。一个合法Agent可以用自己的密钥签署一笔149美元退款,也可以在沙箱里安全地计算金额。可是订单已经超过退款期限时,这笔操作仍然不该执行。业务规则需要一个不依赖模型自觉的确定性检查点。这道检查决定动作是否值得继续。

语义网关可以放在用户消息进入模型之前,也可以放在模型决定调用工具之后。前一处负责识别高风险意图、敏感数据和不允许的请求类型。后一处负责检查工具名、参数、资源归属和审批状态。两边都不应该让模型自己修改检查结果,拒绝结果要由代码直接返回。网关的判断要比模型的解释更有优先级。

在Google ADK工作流里,可以把安全逻辑挂到Runner的插件或回调层,让每个Agent共享同一套策略。Before Tool Callback收到工具名和参数后,先执行金额、订单状态和权限检查。通过以后才进入签名或沙箱流程,失败则返回固定错误类型。这样新增加的Agent不会因为漏写一段提示词而少一道防线。回调结果要能被测试和审计。

网关规则最好使用结构化配置,策略中明确资源类型、金额上限、时间窗口和人工审批条件。规则匹配应有稳定的优先级,不能让模型生成的自然语言覆盖配置结果。每次修改策略都要配套回归用例,至少覆盖正常请求、边界值、缺字段和恶意输入。安全规则也是代码,应该按代码的方式评审。

输出侧同样需要检查,模型返回的内容可能包含内部编号、访问令牌或未经转义的HTML。发送给前端前应做敏感信息过滤和上下文编码,不能把模型输出直接拼进页面。错误信息也要避免泄露数据库结构和密钥路径。输入拦截保护工具,输出拦截保护用户和系统。任何异常都应被记录但不被回显。

语义网关要记录原始请求摘要、命中的规则、工具参数摘要和最终决定,但不要把完整密钥或个人数据写进普通日志。记录拒绝原因时要使用稳定的错误码,方便按版本比较拦截率。审计人员需要看到“为什么拒绝”,开发人员还要看到“哪条规则拒绝”。两种视角都保留,排障速度才不会被牺牲。

网关不能替代人工审批,尤其是付款、删除和批量修改这类不可逆动作。对于超过阈值的请求,系统可以生成待审批任务,冻结原始载荷,等真人确认后再签名执行。审批人看到的内容必须和最终签名的内容完全一致。否则人工确认只是一个漂亮的按钮,并没有真正参与授权。

把三道边界接进 Google ADK 工作流

把三道边界接进工作流时,入口顺序比组件数量更重要。用户请求先经过输入侧网关,再交给ADK决定是否需要调用工具。工具调用前再次检查参数,数据库写入则进入签名服务。动态代码调用则进入隔离运行器,两条路径都把结果带回审计链。每个节点都只承担一种防护责任。

Runner初始化时可以挂载统一安全插件,插件负责注册回调、错误码和审计钩子。每个业务Agent只声明自己需要的工具和资源,不重复实现金额校验。这样策略变化可以集中测试,避免多个Agent出现细微差异。插件本身也要保持小而清晰,不能变成另一个无法审计的模型层。插件越简单,越容易做版本审计。

数据库工具不应接收任意SQL字符串,而应接收有限的动作对象。对象可以包含操作类型、表名、主键、变更字段和请求编号,字段集合由服务端白名单决定。工具把对象交给签名服务,签名服务再交给数据库入口。数据库入口拒绝未知字段,防止模型借一个合法工具拼出未授权操作。

执行Python的工具也需要固定输入输出协议,代码、输入文件和资源上限分别传递。运行器为每次请求创建短生命周期实例,挂载只读文件,关闭网络并设置超时。返回值先经过大小限制和字符清理,再由Agent决定如何向用户解释。任何异常都只表示这次任务失败,不应暴露沙箱内部路径。

高风险工具还要有幂等键和审批状态,模型重试不能制造第二笔退款。签名载荷里携带同一个请求编号,入口服务处理过一次后就拒绝重复消费。审批状态要与载荷摘要绑定,审批之后金额发生变化必须重新走流程。这样重试、网络重连和模型重复规划都不会绕开授权。

可观测性要覆盖模型决定和工具结果之间的空白区域,不能只采集输入输出文本。至少要记录回调触发、网关结果、签名请求、沙箱启动和数据库结果。日志中的关联ID应贯穿一次任务,方便从用户请求追到最终事务。监控指标还要区分模型拒绝、策略拒绝、验签失败和运行超时。

上线前应做一轮故障演练:篡改金额、替换主键、重放旧签名、读取环境变量和制造死循环。每个案例都要有明确的预期状态,包含“没有写入”“没有外连”和“审计可查”。如果只能看到模型说它拒绝了,却看不到系统实际拒绝,验收就还没有完成。安全能力必须落在外部可观测的结果上。

从演示代码走向生产验收

下面的程序可以直接用Python运行,它用本地HMAC模拟签名服务,并把业务阈值放在入口检查里。程序故意构造一次合规请求,再修改金额后重新验签。你会看到签名校验和业务校验是两件事,缺少任何一件都可能放行错误操作。它不启动容器,所以不能代替gVisor的隔离测试。先看结果,再把依赖替换成云服务。

import hashlib
import hmac
import json

AGENT_KEYS = {"refund-agent": b"local-demo-key-change-me"}
MAX_REFUND_CENTS = 100000


def canonical(payload):
    return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")


def sign(agent_id, payload):
    key = AGENT_KEYS[agent_id]
    return hmac.new(key, canonical(payload), hashlib.sha256).hexdigest()


def verify(agent_id, payload, signature):
    key = AGENT_KEYS.get(agent_id)
    if key is None:
        return False
    expected = hmac.new(key, canonical(payload), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


def allow_refund(payload):
    return (
        payload.get("order_owner") == payload.get("requester")
        and payload.get("refund_cents", 0) <= MAX_REFUND_CENTS
        and payload.get("approved") is True
    )


def main():
    payload = {
        "agent_id": "refund-agent",
        "order_owner": "user-7",
        "requester": "user-7",
        "refund_cents": 5000,
        "approved": True,
    }
    signature = sign(payload["agent_id"], payload)
    print("valid:", verify(payload["agent_id"], payload, signature), allow_refund(payload))
    payload["refund_cents"] = 900000
    print("tampered:", verify(payload["agent_id"], payload, signature), allow_refund(payload))


if __name__ == "__main__":
    main()

把这段代码保存后运行,第一行应同时得到两个True,第二行应同时得到两个False。金额改变以后,原签名无法匹配新的载荷,业务阈值也会拒绝请求。这个结果只证明拒绝链的基本关系,不代表本地HMAC具备云端密钥的防护能力。工程团队可以把它作为单元测试的起点。测试通过后还要补充重放和超时用例。

接入真实环境时,先把HMAC替换为Cloud KMS的非对称签名,再为每个Agent配置独立Service Account。接着把动态代码交给带gVisor运行时的容器,明确网络、能力、文件和资源上限。最后把业务规则挂到ADK回调和数据库入口两处。三处都通过以后,才谈自动执行生产动作。每次替换都要保留同样的载荷格式。变更过程也要保留记录。

验收清单还应包含密钥轮换、旧签名拒绝、重复请求拒绝和审批内容不可修改。沙箱测试要验证没有外网、没有敏感挂载、超时会销毁实例。网关测试要覆盖缺字段、边界金额、错误资源和输出泄露。每次策略或模型升级,都应重跑同一组攻击样例。验收记录应与版本号一起保存。

零信任Agent的价值不在于让模型永远不犯错,而在于把犯错的代价压缩在可控范围内。模型负责理解和规划,签名负责身份,沙箱负责环境,网关负责业务。只要权限、运行环境和业务规则都写在模型之外,Agent才有资格碰生产系统。真正的自动化不是放弃控制,而是把控制变成可验证的工程边界。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐