我问 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/ 改成匹配相对路径就行。但问题是:能穷举所有敏感路径吗?
思考
正则穷举的思路本质上是在做"猜测"——猜哪些文件名可能敏感。但安全性不应该依赖猜测:
- 路径可以绕过:相对路径、
../穿越、符号链接 - 文件名不等于内容:
config.json可能包含 API Key,.env可能只是空文件 - 意图很重要:用户主动要求看 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 应用有本质区别:
- LLM 是不可预测的执行者:它可能用你意想不到的方式调用工具,正则穷举永远追不上 LLM 的创造力
- 上下文即泄露:传统应用可以"读后再脱敏",但 AI Agent 的上下文窗口一旦包含密钥就不可挽回
- 纵深防御是必须的:每一层都是独立的,覆盖不同维度(路径、内容、行为)