StoneRen

AI Agent 的安全防线:从 .env 泄露到三层防御

Apr 27, 2026 ·
0

我问 AI Agent ".env 中有哪些信息?",它直接把 API Key 展示了出来。

问题出在一个看似简单的正则表达式上,但它揭示了 AI Agent 安全设计中一个根本性的架构缺陷。

我正在构建 ai-sails。项目有一个权限系统,包含 HITL(Human-in-the-Loop)确认机制,按理说读取 .env 这种敏感文件应该弹出确认框。

但事实是:Agent 调用了 read 工具,路径参数是 .env(相对路径),权限引擎的正则 /\/\.env\b/ 要求 .env 前必须有 /,所以没匹配上。请求直接落到 readOnlyAllowRule,被自动放行。API Key 就这么明文展示了出来。

Important

虽然key已经脱敏了,但是关键信息已经流转了一个遍了

修这个正则很简单,把 /\/\.env\b/ 改成匹配相对路径就行。但问题是:能穷举所有敏感路径吗?

思考

正则穷举的思路本质上是在做"猜测"——猜哪些文件名可能敏感。但安全性不应该依赖猜测:

  1. 路径可以绕过:相对路径、../ 穿越、符号链接
  2. 文件名不等于内容config.json 可能包含 API Key,.env 可能只是空文件
  3. 意图很重要:用户主动要求看 vs Agent 自行决定看

更根本的问题是:AI Agent 的安全性不能只靠一层检查

三层防御架构

最终采用了分层防御的方案,每一层独立、零耦合,单层失效不影响其他层。

Layer 1:路径规则 + 服务器拒绝

这是最直接的防线。核心改动是新增了一条 serverSecretDenyRule

// 服务器自身的配置文件 → 直接拒绝,不给确认选项
export function serverSecretDenyRule(): PermissionRule {
  return {
    name: 'server-secret-deny',
    match: (toolName, args) => {
      const rawPath = String(args.path ?? '')
      const resolved = resolve(rawPath)  // 关键:先解析为绝对路径
      const serverRoot = process.cwd()
      return /\.(?:env|secret|credentials)(?:\.\w+)?$/i.test(resolved)
        && resolved.startsWith(serverRoot)
    },
    action: 'deny',  // 不是 confirm-required,是直接拒绝
  }
}

两个关键设计决策:

  • resolve() 处理路径后再匹配:无论是 .env./.env 还是 /full/path/.env,都会被正确识别
  • action 是 deny 而非 confirm-required:服务器自身的密钥不应该有"授权查看"的选项

这条规则被放在权限引擎的最高优先级,确保在任何 allow 规则之前拦截。

Layer 2:内容扫描守卫

路径检查只能拦截"文件名看起来敏感"的情况。但如果 Agent 读取了 config.json,里面恰好包含 API Key 呢?

const SECRET_PATTERNS = [
  { pattern: /sk-[\w-]{20,}/, label: 'API Key' },
  { pattern: /AKIA[A-Z0-9]{16}/, label: 'AWS Access Key' },
  { pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/, label: 'Private Key' },
  { pattern: /(?:^|[_\s])(?:password|passwd|secret|token)\s*[=:]\s*\S{8,}/i, label: 'Credential' },
  { pattern: /Bearer\s+[\w.~+/\\-]+=*/, label: 'Bearer Token' },
]

内容扫描发生在工具执行之后、结果返回之前。关键决策是:检测到敏感内容直接拒绝,不脱敏。因为 LLM 上下文一旦看到完整密钥,即使后续脱敏也来不及——密钥已经在模型的上下文窗口中了。

这里还有一个工程细节:工具执行有两条路径——主路径经过 orchestrator,确认路径直接调用 tool.execute()。确认路径绕过了 orchestrator,所以 content guard 必须在两个地方都集成。

Layer 3:System Prompt 安全引导

最后一层是让 LLM 自身具备安全意识:

## 安全策略
- 绝不展示服务器配置中的密钥、Token、密码等敏感信息
- 如果用户要求查看敏感信息,说明原因并拒绝
- 读取可能包含敏感内容的文件前,先确认文件用途再决定是否展示

这一层不是技术防线,而是行为引导。它利用 LLM 的指令遵循能力,让 Agent 在调用工具之前就做出安全判断。前两层是"硬防线"(代码级拦截),这一层是"软防线"(行为引导)。

最终效果

用户请求: ".env中有哪些信息?"

    ├─ Layer 3 (System Prompt): Agent 判断这是敏感请求,可能直接拒绝
    │   └─ 如果 Agent 仍决定调用 read({path: ".env"})

    ├─ Layer 1 (路径规则): serverSecretDenyRule 匹配 → deny
    │   └─ 工具不执行,返回 "Access denied"

    └─ 假设 Layer 1 被绕过(比如路径不在匹配规则中)

        └─ Layer 2 (内容扫描): 扫描工具输出 → 发现 API Key → 拒绝展示

三层叠加,任何单层失效都有下一层兜底。

用户是无法看到任何敏感信息的。

经验

AI Agent 的安全设计和传统 Web 应用有本质区别:

  1. LLM 是不可预测的执行者:它可能用你意想不到的方式调用工具,正则穷举永远追不上 LLM 的创造力
  2. 上下文即泄露:传统应用可以"读后再脱敏",但 AI Agent 的上下文窗口一旦包含密钥就不可挽回
  3. 纵深防御是必须的:每一层都是独立的,覆盖不同维度(路径、内容、行为)
PRESENT 2026 ©StoneRenver:2604272131