知识库Agent生产工程生产工程安全、Guardrail 与人工介入安全、Guardrail 与人工介入
04 · 生产工程生产工程
Roadmap 04进阶Markdown78 min

安全、Guardrail 与人工介入安全、Guardrail 与人工介入

从提示注入威胁模型到服务端授权、SSRF 出站控制与不可篡改审批,构建 Agent 纵深防御体系。从提示注入威胁模型到服务端授权、SSRF 出站控制与不可篡改审批,构建 Agent 纵深防御体系。

#SecuritySecurity#OWASPOWASP#GuardrailGuardrail#HITLHITL更新于 2026-08-16

专题导读

Agent 安全与普通聊天机器人最大的差别,是模型不仅“说话”,还可能检索私有数据、访问 URL、执行工具并产生不可逆副作用。模型会把系统指令、用户输入、网页、邮件、RAG 文档和工具返回共同放入上下文,但自然语言中的“高优先级说明”并不是可信执行边界。任何外部内容都可能携带 prompt injection,模型输出也必须被当作不可信提议。

因此,安全设计不能寄希望于一句“忽略恶意指令”的 system prompt。正确思路是纵深防御:代码负责身份认证、资源级授权、参数校验、网络出口和凭证;模型只负责生成候选计划;高风险动作通过与具体计划绑定的审批令牌放行;全链路可审计并可快速撤销。Guardrail、分类器和关键词过滤有价值,但只能降低风险,不能代替访问控制。

知识地图

主体认证 → 输入/文件治理 → 不可信内容隔离 → RAG ACL → 模型候选计划 → 工具 Schema → 服务端授权 → 出站网络策略 → 高风险审批 → 输出编码 → 审计/检测/响应

资产 主要威胁 强制控制位置
系统提示与内部策略 提取、覆盖、侧信道泄露 最小披露、服务端策略、输出检查
私有文档与跨租户数据 越权检索、间接注入、投毒 入库治理、查询前 ACL、租户隔离
工具和业务副作用 过度代理、参数篡改、重放 工具网关、领域服务、幂等、审批
HTTP/浏览工具 SSRF、重定向绕过、元数据访问 URL 规范化、DNS/IP 校验、网络出口
模型输出 SQL/命令/HTML 注入、错误决策 参数化 API、上下文编码、schema 校验
凭证与审计数据 泄露、长期滥用、二次扩散 短期凭证、密钥服务、脱敏和保留期

面试题

Q1:什么是 Prompt Injection?它与传统 SQL Injection 有何异同?

核心回答: Prompt injection 是不可信内容诱导模型偏离预期指令、泄露信息或滥用工具的攻击类别。相同点是控制指令与数据边界被突破;不同点是自然语言模型没有像 SQL 参数绑定那样完备、确定的指令/数据隔离机制,因此“加分隔符”或“告诉模型不要服从”只能缓解,不能证明安全。

直接注入来自用户请求,例如要求忽略原规则;间接注入藏在网页、PDF、邮件、代码仓库、RAG 文档或工具回包中,用户甚至不知道恶意指令存在。攻击目标不一定是获取 system prompt,还可能是让 Agent 选择更高权限工具、把数据发往外部地址、批准错误计划或污染后续记忆。

威胁建模时应把模型视为可能被操纵的规划器,而不是策略执行点。安全不变量——当前主体是谁、能访问哪些资源、能调用哪个操作、参数范围是什么——必须由模型之外的服务端代码验证。OWASP 的 Prompt Injection 风险说明也强调直接和间接注入及其对工具、数据的影响。

面试追问: 把外部文本放在 XML 标签里是否安全?标签能帮助模型理解结构,但攻击文本仍在上下文内,不能作为授权边界。

常见错误回答: “过滤 ignore previous instructions 就行”;攻击可改写、编码、跨语言或藏在多模态内容中。

Q2:为什么间接 Prompt Injection 对 RAG Agent 特别危险?

核心回答: RAG 主动把第三方内容提升到模型上下文,扩大了不可信输入面;若 Agent 同时拥有读取私有数据和外发/写入工具,恶意文档就可能把“读到信息”升级为“执行动作”。这是数据来源、模型解释和工具权限组合后的系统风险。

防御要覆盖入库和查询两端。入库记录来源、发布者、抓取时间、内容哈希、版本和扫描结果;敏感知识库建立发布审核。查询时先依据调用主体执行租户与文档级 ACL,再检索,而不是召回后让模型自行忽略无权内容。返回模型时标注来源与不可信属性,限制该任务可见的工具集合。

即使扫描未发现攻击,也不能放宽工具授权,因为检测存在漏报。对于“读取网页并总结”任务,通常不应同时暴露邮件发送、云资源修改等无关工具。高风险 RAG 可把检索与执行分成两个隔离阶段,并要求证据引用和人工确认。

面试追问: 文档来自公司内部是否可信?内部账号、同步源和供应链也可能被攻破,至少仍需 provenance、ACL 和最小权限。

常见错误回答: “向量库是内部系统,所以召回内容可信”。存储位置不等于内容可信。

Q3:模型、Guardrail、工具网关、领域服务分别承担什么安全责任?

核心回答: 模型提出候选计划;Guardrail 识别风险信号;工具网关强制工具 allowlist、schema、限额和审批;领域服务基于权威身份与业务状态做最终授权。越靠近资源的组件越拥有最终决定权。

模型可以判断“这看起来像退款请求”,但不能决定用户是否拥有订单。Guardrail 可以标记疑似密钥或攻击语句,但误报、漏报不可避免。工具网关负责注入不可由模型覆盖的 subjectId/tenantId,裁剪参数,校验 action scope。领域服务仍要检查订单归属、状态机和金额,不能因为请求来自“可信 Agent 网关”就跳过授权。

这种职责划分让模型失陷时损害受限。工具定义应细粒度,例如 readOrderrefundOrder 分开,不能用一个接受任意 URL、HTTP 方法和 body 的“万能工具”承载全部权限。

面试追问: 工具网关已经授权,领域服务能否不再检查?不能。纵深防御且领域服务掌握最新资源状态;还要防网关配置错误和内部绕过。

常见错误回答: “在 system prompt 写清楚权限即可”;自然语言约束不是强制访问控制。

Q4:如何防止 Agent 越权和跨租户数据泄露?

核心回答: 认证后的主体与租户信息由服务端可信上下文传递,绝不接受模型或用户在工具参数中自报;每次检索和工具调用执行对象级授权;存储、缓存、索引与日志都要保持租户隔离。

工具调用参数可只包含业务对象标识,网关从会话注入 subjectId,领域服务查询 resource.tenantId == context.tenantId 并检查 RBAC/ABAC 策略。列表接口必须在数据库查询中带权限谓词,不能先取全量再让模型过滤。RAG 也同理:ACL 过滤应在可返回候选集合之前生效。

缓存 key 包含租户、主体或权限版本;共享缓存只保存公开且与主体无关的结果。trace、评测集和向量库可能形成第二份敏感数据副本,也必须应用相同隔离和删除流程。

面试追问: 只读工具为什么仍是高风险?读取客户、财务或凭证数据后,模型可在响应或另一个外发工具中泄露。

常见错误回答: “用户知道对象 ID 就能访问”;对象 ID 不是授权凭证。

Q5:怎样安全处理模型输出,避免 SQL、Shell、HTML 或业务注入?

核心回答: 模型输出始终是不可信数据。先解析成有限 schema,再做类型、枚举、范围、资源状态和授权校验;最终使用参数化接口。不要把模型文本直接拼进 SQL、Shell、模板、URL 或策略表达式。

数据库操作使用预定义 repository 方法与参数绑定;系统命令尽量不用 shell,必要时调用固定可执行文件和独立参数数组;HTML 根据具体上下文编码,文本、属性、URL、JavaScript 上下文不能共用一个“转义函数”;业务动作使用枚举而非任意方法名。JSON schema 通过也只证明结构正确,不证明语义和权限正确。

对自由文本输出还要检查敏感数据、引用依据和目标渠道。若模型生成 Markdown,渲染器应 sanitize;若生成邮件,收件人、附件和数据分类由服务端检查。

面试追问: 让模型二次自检能否替代代码校验?不能。它可作为额外信号,但可能受到相同攻击或犯相同错误。

常见错误回答: “输出是 JSON 就安全”;JSON 中仍可包含越权对象、危险 URL 和恶意字符串。

Q6:Agent 的 SSRF 风险从哪里来?完整防线是什么?

核心回答: 当模型可控制抓取 URL、Webhook、图片下载、插件回调或重定向时,攻击者可诱导服务访问内网、环回、云元数据、管理面或其他不可公开资源。URL allowlist 只是第一层,必须配合规范化、DNS/IP 检查、重定向重验和网络层出站限制。

应用层应只允许 https 等必要协议,禁止用户信息段和非预期端口,使用标准 URI 解析而非字符串前缀;对 host 解析所有 A/AAAA 地址,拒绝 loopback、link-local、site-local、私网、保留和元数据地址。建立连接时仍要防 DNS rebinding:通过受控代理/解析器固定已验证目的地址,并校验证书主机名。每次重定向重新执行完整策略,限制跳数和响应大小。

真正强边界是 egress proxy、容器网络策略和防火墙:Agent worker 默认不能访问内网与云元数据,只能访问明确批准的外部服务。不要依赖 HTTP 客户端默认重定向。OWASP 的 SSRF Prevention Cheat Sheet提供了应用与网络双层控制思路。

面试追问: 只允许 example.com 是否足够?还要考虑子域接管、DNS 解析变化、重定向、IDN/尾点/混合 IP 表示及批准域本身的开放代理。

常见错误回答: “拒绝 127.0.0.1 就可以”;IPv6、整数/十六进制表示、私网和元数据地址仍可绕过幼稚过滤。

Q7:工具凭证和密钥应如何管理?

核心回答: 模型上下文中不应出现长期密钥。工具由后端使用工作负载身份或密钥服务换取短期、最小 scope、限定受众的凭证;凭证不写 prompt、向量库、普通 trace 或错误信息。

每个工具使用独立服务身份和权限,避免一个万能 API key。高风险任务可签发绑定租户、操作和短有效期的 delegation token。工具网关在服务端注入凭证,模型只看到逻辑工具名。密钥要支持轮换、撤销、使用审计和异常检测。

供应商调用还要明确数据处理边界:哪些字段离开组织、驻留区域、保留策略和是否用于训练。敏感数据在进入模型前最小化,不能把“供应商已签协议”理解为无限制发送。

面试追问: 为方便重放,能否把凭证加密后记在 trace?通常不应。重放应重新获取当前授权下的新凭证,而不是复用历史秘密。

常见错误回答: “system prompt 不会展示给用户,所以可以放 key”。模型输出、日志和供应商侧处理都可能泄露。

Q8:哪些操作需要 Human-in-the-loop?如何避免审批流沦为“点确认”?

核心回答: 根据影响、可逆性、金额、数据敏感度和不确定性分级。转账、删除、对外发布、批量发信、权限变更和生产配置修改通常需要审批;低风险只读或可轻易撤销操作可自动执行。审批页面必须展示具体主体、目标、参数、影响范围、证据和有效期。

审批人要有对应资源权限,而不是任何登录用户都能批。界面应突出与风险有关的差异,避免把大段模型文本变成确认疲劳。批量操作明确数量、筛选条件和最大影响;审批后计划变化必须重新申请。

高风险场景可采用双人审批、职责分离或时间延迟。拒绝与超时也是明确状态,worker 不得自动把审批超时当批准。审批服务故障时默认 fail closed,并允许安全取消。

面试追问: 所有工具调用都人工批准是否更安全?会造成疲劳和绕过,降低真正高风险审批的质量;应按策略分级。

常见错误回答: “弹一个 Are you sure? 就是 HITL”。没有展示具体计划和绑定执行内容,确认没有安全意义。

Q9:什么是审批绑定?如何防止“批准 A,执行 B”和 TOCTOU?

核心回答: 审批令牌应绑定 runId、动作类型、规范化参数摘要、资源版本、审批主体、过期时间和唯一 nonce,并由审批服务签名。执行前工具网关重新计算摘要、核验审批人权限、检查令牌未过期/未消费及资源版本未变化。

规范化很关键:JSON 字段顺序、默认值和集合顺序若不统一,同一计划会产生不同摘要;反之也不能忽略有安全意义的字段。一次性 nonce 在原子存储中消费,防止重放。若订单金额、收件人列表或目标资源在批准后改变,乐观锁版本不匹配,必须重新审批。

审批令牌不是业务授权替代品。执行时仍要检查发起人权限和当前业务规则,因为权限可能已撤销。审计记录保存计划摘要、审批与执行结果,但不保存无必要的敏感正文。

面试追问: 只绑定 prompt 文本可以吗?不可以。相同自然语言可生成不同工具参数;应绑定最终规范化执行计划。

常见错误回答: “runId 相同就视为已批准”;同一 run 中计划可重规划,必须绑定动作和参数。

Q10:怎样设计输入、文件和输出 Guardrail?边界在哪里?

核心回答: Guardrail 是风险检测与格式约束层,包括大小/类型限制、恶意文件扫描、内容分类、DLP、schema 校验和输出渠道策略。它能降低攻击成功率和误操作,但不能证明内容安全,也不能替代资源级授权、沙箱与网络策略。

上传文件要验证实际 MIME/魔数、解压大小、嵌套深度、页数和解析时间,防止压缩炸弹与解析器 DoS;转换服务运行在隔离环境。输入分类器的命中用于拒绝、降权、移交人工或限制工具,而不是直接把未命中内容标为可信。

输出侧按目的地处理:用户界面 sanitize;日志脱敏;外发邮件检查收件人与数据分类;工具调用做 schema、领域和授权校验。模型拒答也应被评测,避免过度防御导致合法任务大面积失败。

面试追问: 分类器达到 99% 准确率是否足够?需要看攻击基率、漏报/误报成本、切片表现和绕过能力,单一准确率没有决策意义。

常见错误回答: “用了一个安全模型,所以后续都可信”。

Q11:如何进行 Agent 安全测试、监控和事件响应?

核心回答: 建立可重复的威胁驱动测试集,覆盖直接/间接注入、跨租户访问、工具参数篡改、SSRF、敏感数据回显、审批重放和降级路径;线上监控策略拒绝、异常工具序列、外发目的地、权限失败与凭证使用,并准备撤销和隔离能力。

红队结果要转成自动回归样本,绑定模型/prompt/工具版本。测试不能只问模型会不会泄露 system prompt,还要验证真实强制边界:越权对象是否返回 403、私网连接是否在网络层失败、旧审批令牌是否拒绝、重复消息是否只产生一次副作用。

事件响应先限制损害:停用特定工具或租户路线、撤销短期凭证、阻断 egress、隔离被投毒索引版本、保留最小必要证据。随后确定受影响数据和动作,通知责任方,修复控制并将样本纳入回归。NIST AI RMF Generative AI Profile可用于把风险识别、测量、管理和治理连接起来。

面试追问: 线上发现攻击 prompt,是否应永久保存原文?依据数据分类、事件证据需求和保留政策;默认不应无期限扩散攻击内容和敏感数据。

常见错误回答: “升级 system prompt 后就结案”。如果授权或网络边界未修复,同类攻击仍可换表达绕过。

Q12:系统设计题——设计一个支持高风险写操作的安全 Agent 工具网关

核心回答: 网关接收模型生成的候选工具调用,但从可信会话注入主体/租户;按任务下发最小工具集合;执行 schema、领域前置校验、资源级授权和限额;高风险动作生成规范化计划并转审批服务;审批令牌校验通过后再调用领域服务,全部步骤进入持久、访问受控且可校验完整性的防篡改审计链。普通 trace 或日志并不自动具备密码学、身份或法律意义上的“不可抵赖”。

核心组件包括:Policy Decision Point、Tool Registry、Schema Validator、Credential Broker、Approval Service、Egress Proxy、Audit Sink 和 Kill Switch。工具元数据记录风险等级、scope、超时、最大请求大小、是否幂等和审批策略。外部平台通过窄接口 adapter 接入,不能向模型暴露任意 HTTP。

可用性边界要明确:策略或审批服务不可用时,高风险写操作 fail closed;只读低风险路径可按缓存策略有限降级。审计失败时是否阻断取决于风险级别,但必须有本地有界缓冲和丢失告警。网关不替代领域服务授权,也不保存长期供应商密钥。

面试追问: 如何防内部调用绕过网关?领域服务继续鉴权;网络与服务身份策略限制只有批准工作负载可访问写接口;审计比较入口与领域事件发现旁路。

常见错误回答: “网关统一拼一个安全 system prompt”;这不是强制执行架构。

Q13:事故排查题——Agent 访问了云元数据地址并疑似泄露凭证,怎么办?

核心回答: 按凭证泄露处理,而不是只封 URL。立即隔离相关 worker、阻断 egress 和元数据访问、撤销/轮换可能暴露的凭证、停用受影响工具;随后以 trace、代理日志和云审计确定请求链、返回内容和凭证使用范围。

排查输入来源:用户 URL、间接注入文档、重定向还是工具配置;核查应用层是否只做字符串检查、HTTP 客户端是否自动重定向、DNS/IP 是否在连接前变化、容器是否有网络层限制。搜索泄露凭证之后的异常 API 调用,按最坏边界通知和修复。

永久修复包括默认拒绝出站、受控代理、地址分类、每跳重验、禁用元数据或要求更强元数据访问机制、缩短凭证生命周期与 scope。将攻击链加入回归:应用校验、代理和网络层必须分别拒绝。

面试追问: 日志没有响应 body,能否认定未泄露?不能。还需查字节数、下游模型请求、外发工具和云审计;缺少证据不是安全证据。

常见错误回答: “把 169.254.169.254 加黑名单就结束”;其他地址表示、IPv6、内网服务和重定向仍存在。

Java 21 完整示例:绑定执行计划的一次性审批令牌

示例只用 Java 21 标准库,展示规范化计划、HMAC 签名、参数摘要、过期检查和一次性消费。外部业务平台通过 ToolExecutor 接口抽象。示例的 InMemoryNonceStore 仅适合单 JVM;生产应使用支持原子 compare-and-set、TTL 和多副本一致性的共享存储。设参数数为 p、规范化后总字节数为 L、有效 nonce 数为 n,签发和验证时间为 O(p log p + L),单次临时空间为 O(p + L),nonce store 持久空间为 O(n)

JAVA
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public final class BoundApprovalExample {
    private static void appendField(StringBuilder output, String field) {
        Objects.requireNonNull(field);
        byte[] encoded = field.getBytes(StandardCharsets.UTF_8);
        output.append(encoded.length).append(':').append(field);
    }

    record ActionPlan(
            String runId,
            String action,
            String resourceId,
            long resourceVersion,
            Map<String, String> parameters) {
        ActionPlan {
            Objects.requireNonNull(runId);
            Objects.requireNonNull(action);
            Objects.requireNonNull(resourceId);
            parameters = Map.copyOf(parameters);
        }

        String canonicalForm() {
            StringBuilder value = new StringBuilder();
            appendField(value, runId);
            appendField(value, action);
            appendField(value, resourceId);
            appendField(value, Long.toString(resourceVersion));
            appendField(value, Integer.toString(parameters.size()));
            // TreeMap 固定键顺序;长度前缀让换行等任意 UTF-8 内容无歧义。
            new TreeMap<>(parameters).forEach((key, item) -> {
                appendField(value, key);
                appendField(value, item);
            });
            return value.toString();
        }
    }

    record ApprovalToken(
            String planDigest,
            String approverId,
            String nonce,
            Instant expiresAt,
            String signature) {}

    interface NonceStore {
        boolean consumeOnce(String nonce, Instant expiresAt);
    }

    static final class InMemoryNonceStore implements NonceStore {
        private final Set<String> consumed = ConcurrentHashMap.newKeySet();

        @Override
        public boolean consumeOnce(String nonce, Instant expiresAt) {
            // 演示原子一次性消费;生产存储还应自动按 expiresAt 清理。
            return consumed.add(nonce);
        }
    }

    interface ToolExecutor {
        String execute(ActionPlan plan, String subjectId);
    }

    static final class ApprovalService {
        private static final String HMAC = "HmacSHA256";
        private final byte[] key;
        private final Clock clock;
        private final NonceStore nonceStore;

        ApprovalService(byte[] key, Clock clock, NonceStore nonceStore) {
            if (key.length < 32) throw new IllegalArgumentException("use at least 256-bit key");
            this.key = key.clone();
            this.clock = Objects.requireNonNull(clock);
            this.nonceStore = Objects.requireNonNull(nonceStore);
        }

        ApprovalToken issue(ActionPlan plan, String approverId, Duration ttl) {
            // 15 分钟只是本示例策略;生产值按动作风险和资源变化速度配置。
            if (ttl.isNegative() || ttl.isZero() || ttl.compareTo(Duration.ofMinutes(15)) > 0) {
                throw new IllegalArgumentException("invalid approval ttl");
            }
            String digest = digest(plan.canonicalForm());
            String nonce = UUID.randomUUID().toString();
            Instant expiresAt = clock.instant().plus(ttl);
            String payload = payload(digest, approverId, nonce, expiresAt);
            return new ApprovalToken(digest, approverId, nonce, expiresAt, sign(payload));
        }

        void verifyAndConsume(ActionPlan currentPlan, ApprovalToken token) {
            if (!clock.instant().isBefore(token.expiresAt())) {
                throw new SecurityException("approval expired");
            }
            String actualDigest = digest(currentPlan.canonicalForm());
            if (!MessageDigest.isEqual(
                    actualDigest.getBytes(StandardCharsets.US_ASCII),
                    token.planDigest().getBytes(StandardCharsets.US_ASCII))) {
                throw new SecurityException("plan changed after approval");
            }
            String expected = sign(payload(token.planDigest(), token.approverId(),
                    token.nonce(), token.expiresAt()));
            if (!MessageDigest.isEqual(
                    expected.getBytes(StandardCharsets.US_ASCII),
                    token.signature().getBytes(StandardCharsets.US_ASCII))) {
                throw new SecurityException("invalid signature");
            }
            if (!nonceStore.consumeOnce(token.nonce(), token.expiresAt())) {
                throw new SecurityException("approval already consumed");
            }
        }

        private static String payload(
                String digest, String approverId, String nonce, Instant expiresAt) {
            StringBuilder value = new StringBuilder();
            appendField(value, digest);
            appendField(value, approverId);
            appendField(value, nonce);
            appendField(value, expiresAt.toString());
            return value.toString();
        }

        private static String digest(String value) {
            try {
                byte[] bytes = MessageDigest.getInstance("SHA-256")
                        .digest(value.getBytes(StandardCharsets.UTF_8));
                return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
            } catch (Exception error) {
                throw new IllegalStateException(error);
            }
        }

        private String sign(String value) {
            try {
                Mac mac = Mac.getInstance(HMAC);
                mac.init(new SecretKeySpec(key, HMAC));
                return Base64.getUrlEncoder().withoutPadding()
                        .encodeToString(mac.doFinal(value.getBytes(StandardCharsets.UTF_8)));
            } catch (Exception error) {
                throw new IllegalStateException(error);
            }
        }
    }

    public static void main(String[] args) {
        byte[] demoKey = new byte[32]; // 演示占位;生产从密钥服务加载随机密钥。
        ApprovalService approvals = new ApprovalService(
                demoKey, Clock.systemUTC(), new InMemoryNonceStore());
        ActionPlan approved = new ActionPlan(
                "run-42", "refund", "order-1001", 7,
                Map.of("amountCents", "2500", "currency", "CNY"));
        ApprovalToken token = approvals.issue(approved, "approver-9", Duration.ofMinutes(5));

        // 执行前仍应检查 subjectId 和 approverId 对资源的当前权限。
        approvals.verifyAndConsume(approved, token);
        ToolExecutor executor = (plan, subjectId) ->
                "executed " + plan.action() + " for " + subjectId;
        System.out.println(executor.execute(approved, "user-18"));
    }
}

该示例解决“令牌是否批准当前计划”和“是否被重放”,但没有替代资源授权,也没有解决审批人与发起人是否有权、密钥轮换、跨节点 nonce 存储和审计持久化;这些是生产边界。

速记总结

  • 所有用户、网页、邮件、RAG 文档和工具返回都按不可信数据处理。
  • 分隔符、关键词和分类器是缓解措施,不是授权边界。
  • 主体、租户和 scope 由服务端注入;检索、缓存和领域服务都执行资源级 ACL。
  • 模型输出先过 schema、领域约束和授权,再进入参数化 API。
  • SSRF 防护需要 URL 规范化、DNS/IP 与重定向重验、受控代理和默认拒绝的网络出口。
  • 长期密钥不进入 prompt;工具使用最小权限、短期凭证。
  • 审批绑定最终规范化计划、资源版本、过期时间和一次性 nonce;计划变化即重新审批。
  • 事故响应要能停工具、撤凭证、断出口、隔离索引并保留最小必要证据。

参考资料