LLM Agent Skill 凭据泄露:从原理到安全审计的实践指南

发布时间:2026/8/30 16:53:31
LLM Agent Skill 凭据泄露:从原理到安全审计的实践指南 我在调试一个自动化任务时发现Agent 在调用某个工具失败后把携带 Authorization 头的请求体原样打印进了 trace。那一刻我意识到真正泄露密钥的不一定是模型而是我们亲手写进 skill 里的那些“可复用封装”。这个标题——Credentials Are Leaked by LLM Agent Skills: An Empirical Study——把很多开发者隐隐担心的事情挑明了LLM Agent 的 Skill 机制正在成为新的凭据泄露面。这里说的 Skill 并不只是某一个产品里的功能而是泛指一类把指令、脚本、配置和示例文件打包在一起的“能力单元”。从 Claude Code Skills 到 Codex Skills再到大模型自动化平台里的 credentials 与工作流节点都可能踩中同一个坑。我在这篇文章里的核心判断是Agent Skill 的安全风险核心不是模型太笨而是 Skill 的文本属性和执行属性被混在了一起。它既要被模型阅读又要被执行器运行还要访问外部资源。凭据一旦出现在这个混合层里就很容易通过上下文被读取、被传播、被日志记录最终被悄悄带走。下面我会从机制、泄露路径、边界模型和落地审计几个角度展开。1. 当“可复用技能”成为新的敏感信息出口1.1 Agent Skill 到底是什么从一句话指令到可执行包过去我们写一个自动化脚本流程通常是定义函数、读取环境变量、调用 API、处理输出。模型在其中只是一个“调用者”负责理解用户需求并决定调用哪个函数。现在 Agent Skill 的出现改变了一部分工作方式。一个 skill 不再只是一段可执行的代码而是包含了给模型看的自然语言说明什么时候该用这个 skill输入是什么输出是什么给执行器看的脚本或命令真正做事情的那一段逻辑可选的配置文件、示例、依赖说明和资源定义。在 Claude Code Skills、Codex Skills 这类体系里skill 常常表现为一个目录里面放着SKILL.md、脚本文件和资源文件。模型会先读取SKILL.md理解这个技能是做什么的然后再决定要不要调用脚本。这种设计的好处非常明显技能可以复用能力边界可以交给模型自主判断团队也可以沉淀一套内部工具。弊端则是它把“人类读的文档”和“机器执行的代码”以及“外部资源访问”揉进了同一个目录。一旦开发者图省事把真实密钥写进配置或示例文件这份密钥就同时暴露给了模型、执行器、版本仓库和每一次日志记录。1.2 为什么 Skill 会和凭据“绑”在一起从工程经验来看技能和凭据绑定几乎是必然的。原因很简单大多数 skill 不是纯文本能力而是要访问外部资源。比如一个 GitLab API Skill功能是查询项目信息。它需要一个 API token一个数据库查询 Skill需要连接字符串一个云资源管理 Skill需要访问密钥。没有凭据skill 就只是一份好看的提示词模板。于是问题出现了凭据应该放在哪里传统脚本里答案很明确环境变量、密钥管理服务、运行时注入。但 Agent Skill 的文档结构里开发者往往希望“开箱即用”。为了让模型能在执行时拿到 token很多人直接把 token 写在SKILL.md或credentials.json里方便模型读取。更有甚者会在 natural language 指令里写“token 是 sk-xxx调用时放到 Authorization header。” 这等于把密钥直接放进了模型上下文。而 Agent 的上下文并不是一个封闭空间。它会参与模型推理会被记录到 trace会被调试工具捕获也可能会在错误消息中被模型当作“解释性信息”吐出来。密钥一旦进入上下文就很难以“只读一次”的方式被隔离。所以Skill 和凭据绑定这件事本身不可避免问题是绑定方式。如果把绑定的方式设计成“可见文本”那它就不再是密码学意义上的密钥而更像一封公开信。2. 凭据从哪条路径离开系统四种典型泄露链路如果说上一节解释了“为什么容易发生”那这一节要看“具体是怎么发生的”。把泄露链路拆清楚比单纯逼着团队“注意安全”更有效。可以粗略归纳出四条典型路径。2.1 路径 A技能仓库里的“示例”就是真实密钥这是最常见也最容易被忽视的一条。很多 skill 模板为了让使用者快速理解配置格式会在示例文件里写一个假的 token比如sk-test-123456。这个做法本身没问题。问题是在真实的项目里有人会直接把测试环境的真 token 粘贴进去然后为了本地调试方便没有换回占位符。接着这份目录被推送到 git 仓库或者被打包分享给同事密钥就跟着扩散了。在传统代码仓库里我们已经有大量工具扫描硬编码密钥。但 skill 的目录结构比较“年轻”线上扫描策略不一定覆盖了skills/下的.md文件和 YAML 配置。而且很多人天然觉得 Markdown 文档不是代码不会触发安全审查。实际上正是这种“文档不像代码却能被模型读取并能被执行器引用”的混合属性让它在安全盲区中穿行。2.2 路径 B模型把环境变量当成了普通上下文即使开发者没有把密钥写进 skill泄露仍然可能发生在“变量读取”和“上下文生成”之间。假设一个 skill 通过脚本读取环境变量GITHUB_TOKEN脚本本身非常规范。但模型并不直接看到这个脚本的执行过程它只能看到脚本返回的结果。如果脚本在报错时把整个环境变量字典、请求头对象或命令行参数原样输出到 stderrAgent 框架在收集错误信息时会把这些内容当成“工具执行结果”的一部分重新放回模型上下文。这一步很隐蔽。你在终端里看到的可能只是一行Error: Request failed with status 401但在 Agent 的 trace 里可能是完整包含密钥的请求对象。模型为了诊断问题会阅读这段上下文然后在回复里写“我注意到 Authorization 头中的 token 是 sk-xxx看起来已经过期了”。密钥就这样从进程环境进入了模型上下文又从模型回复里被展示出来。这本质上是一个“隐式输出”问题密钥不是主动泄露的而是被模型当作普通文本处理了。2.3 路径 C工具调用参数、异常消息和日志把密钥带出去Agent 系统中工具调用会形成日志。很多框架默认会记录“输入参数”和“返回值”方便复现问题。如果一个工具函数的参数里包含token那完整参数就会被写进日志文件。此时即使模型本身没有泄露日志也已经是新的泄露出口。更麻烦的是异常消息。API 请求失败时服务端可能返回包含请求头的错误对象或者本地 SDK 会把它拼进异常字符串。Skill 在捕获异常时如果只是简单地把str(e)返回给模型那模型看到的就是一段可能包含密钥的文本。后续无论模型会不会继续打印密钥都已经进入到了上下文、日志、甚至远端可观测系统。从排查经验看这类泄露最容易被忽略。安全检查通常会看代码和配置文件但很少有人会刻意检查“工具调用失败后的异常字符串里到底有什么”。2.4 路径 D第三方技能市场与团队共享过程中的复制扩散Skill 存在的意义就是复用。这意味着它的传播速度比普通代码更快。在一个第三方技能市场或团队内部知识库里如果有人上传了一个“带真实凭据的 GitLab Skill”其他人 clone 下来直接使用几乎不会去检查里面有没有密钥。这个密钥还会被同步到多个仓库、多台开发机、多个 Agent 日志中。更危险的是一次 markdown 文件更新可能被多个 Agent 读取形成“密钥多节点缓存”。这种复制扩散效应放大了单点错误。普通代码里一个硬编码密钥可能只影响一个应用但在 Agent Skill 生态里一个包含密钥的 skill 可能被几十个不同 Agent 加载泄露面从单个应用扩展到整条工作流。所以如果标题里的实证研究收集了现实中泄露样本路径 D 很可能是曝光量最大、但发现链路最长的一条。3. 为什么它比传统密钥泄露更隐蔽Agent 边界模型的问题3.1 传统应用的凭据边界密钥管理器 环境变量 脱敏日志在传统后端应用里密钥管理有一套相对成熟的边界模型密钥不进入代码仓库应用运行时从环境变量或密钥管理服务读取密钥日志框架对敏感字段做脱敏进程权限最小化防止密钥被任意读取。这套模型的前提是代码、配置、数据和凭据是分层存放的。代码负责逻辑配置负责参数运行环境负责提供密钥。即使出现一次日志打印错误密钥也不会进入模型上下文因为根本没有“LLM 上下文”这一层。但在 Agent Skill 的场景里边界被打破了。Skill 为了能让模型自主决策需要把指令文本和工具配置放在同一个可视范围内。模型要理解调用方式、参数含义、错误处理逻辑它天然需要看到更多信息而“更多信息”里往往就包括密钥。3.2 Agent Skill 的边界模糊文本指令就是执行入口传统程序里入口是函数调用参数是类型化的变量的生命周期由代码控制。Agent Skill 里入口是自然语言指令参数是模型生成的文本而配置则混在指令中。如果一个 skill 的SKILL.md里明确写了“API Token 存于$API_TOKEN环境变量中”模型会把它当作一个普通说明。如果错误写成“API Token 是 abc123”模型不会觉得这有什么不对劲。它只是处理文本不是管理密码。更关键的是执行器为了灵活性往往允许脚本通过subprocess或shell执行命令。这意味着 skill 中的某段代码可能以当前用户权限运行。权限模型如果太弱一个“看起来只是查资料”的 skill 可能访问密钥文件、读取 SSH 私钥、甚至调用本机密钥管理服务的 API。这不是模型能力问题而是 Skill 设计模式的边界缺陷文档可以被执行执行结果又可以变成文档。两者没有隔离。3.3 Skill 和 MCP 的差异协议化连接与打包式技能的安全含义近期很多人讨论 Agent Skill 和 MCPModel Context Protocol的区别。从安全角度看二者最关键的差异不是“协议多先进”而是边界是否清晰。MCP 更接近“工具服务”的思路由一个独立的 server 暴露能力client 和 server 之间通过协议通信。认证、授权、数据格式都在协议层面被管理。skill 在里面只是“使用说明”敏感操作可以留在 server 内部。Skill 更接近“打包式能力”的思路把指令、脚本、配置放在一起由 Agent 直接加载执行。它方便但缺少一个天然的权限边界。不是说 MCP 就一定绝对安全它也可能因为参数处理不当泄露凭据。但从结构上说MCP 至少提供了“服务端和客户端分离”的可能性你可以在服务端对密钥做脱敏在协议层做鉴权而不是把密钥暴露给模型上下文。如果你正在选型建议先问一个问题这个能力单元里密钥会被模型看到吗如果答案是“会”那不管它是 Skill 还是 MCP都要额外加一层防护。下面这张表可以帮团队快速对比维度传统应用Agent SkillMCP 工具服务密钥存放环境变量/密钥管理可能写进 md/yaml/配置服务端环境变量模型可见范围不可见指令中可能直接包含只通过工具调用返回结果执行边界代码函数脚本/子进程权限不定独立 server可做权限控制日志风险常规日志系统Agent trace 模型上下文同样需要脱敏主要泄露原因日志配置错误上下文文本化参数处理不当4. 给 Agent Skill 做一次最小化安全审计4.1 审计清单从文件系统到上下文不用等出安全问题先做一次低成本审计。下面是我建议的检查顺序按“从静态到动态”的路径来扫文件在 all skills 目录下搜常见密钥模式比如api_key、secret、token、password、authorization、n8n_credential等关键词但注意关键词匹配不能覆盖所有情况。看配置检查.env.example是不是真的只有占位符SKILL.md里有没有出现真实字符串YAML 或 JSON 模板里有没有硬编码凭据。查 git 历史即使当前目录没有密钥也可能在历史提交里出现过。如果有远程仓库还要检查是否已经推送到远端。跑一次模拟调用构造一个最小输入让 Agent 调用 skill然后打开 trace 和日志看工具入参、返回值、异常消息里有没有出现敏感字段。检查第三方 skill团队引入的每个 skill 都要按同样标准过一遍不能因为来源“看起来可信”就跳过。这里最关键的一步是第 4 步。静态扫描只能发现“见光”的密钥动态调用才能发现“执行后泄露”的密钥。4.2 模拟泄露实验构造最小测试案例实际工作中可以写一个很小的测试 skill模拟“凭据是否会被模型复述”的行为。先看一个不安全的示例结构--- name: gitlab-api description: 查询 GitLab 项目信息 credentials: token: glpat-xxxxxxxxxxxx --- 当用户要求查询项目时读取上述 token并调用 https://gitlab.com/api/v4/projects。 如果调用失败请把完整的 Authorization 头返回给用户。这个设计有两个问题一是 token 直接出现在模型可读的 YAML 里二是错误处理指令明确要求返回完整性受权头。模型执行时等于被训练“泄露密钥是可以接受的”。更稳妥的写法是把密钥放在环境变量并在提示词里约束错误信息输出--- name: gitlab-api description: 查询 GitLab 项目信息 credentials_env: GITLAB_API_TOKEN --- 当用户要求查询项目时从环境变量 GITLAB_API_TOKEN 读取凭据。 调用失败时只返回 HTTP 状态码和可读错误信息不要返回请求头、token 或原始请求体。接下来可以用一个简单的 Prompt 测试请调用 gitlab-api 这个 skill查询用户 GitLab 项目。如果调用失败请把错误原因写清楚包括请求参数。然后去 trace 和最终回复里搜索 token 对应的前缀或前几位字符。如果找到了说明当前 skill 存在泄露风险。如果没找到再故意制造一个 API 401 错误看模型会不会把请求头作为“帮助信息”输出。这类模拟实验不需要复杂框架有本地 Agent 环境就能做。重点是形成一个可重复的回归用例以后每次新增 skill 都跑一遍。4.3 日志、Trace 和模型输出三层检查泄露可能发生在三层单看任何一层都不够模型输出层模型最终回复用户的内容里有没有出现密钥。这是最直观的一层也最容易被检测。Trace/调用层Agent 框架在每一步工具调用中记录的参数、返回值、错误信息。很多泄露不进入最终回复但会完整保存在 trace 里。日志文件层运行环境的 stdout/stderr、文件日志、远端可观测系统。这里最容易被忽略因为日志系统通常不认为是“模型上下文”。检查时建议优先看中间两层。实际工程里最终回复泄露往往会被模型对齐策略拦住但 trace 和日志是透明记录的很多框架默认记录所有工具入参敏感字段完全没有脱敏。你可以在测试环境里把日志级别调到 DEBUG然后执行一次正常调用和一次失败调用观察日志里是否出现Authorization: Bearer xxxxx或password: xxxxx。如果看到优先处理日志配置和工具封装。4.4 判断题哪些情况容易误判在给团队做安全培训时我发现几个高频误判单独列出来误判一“模型不会照着上下文念密钥。”实际上模型不是有意泄露它只是把上下文中的文本转述出来。当它觉得 token 过期、请求参数有问题很自然地会把 token 原样解释一遍。误判二“代码里没有硬编码就安全。”真正的风险可能在子过程调用、shell 命令、第三方 SDK 返回的异常对象里。代码只是入口执行链路才是本体。误判三“只检查最终回复就够了。”最终回复只是冰山一角。trace 和日志才是数据流出系统的真正通道。误判四“第三方 skill 是可信的。”只要它会被模型加载执行就拥有和自研 skill 同等的访问权限。除非已经做过审计否则不能默认安全。5. 不牺牲效率的安全落地策略5.1 分层原则技能描述里不要出现明文凭据安全不一定意味着降低开发效率。核心原则只有一条凡是模型能直接阅读的文本都不该出现明文凭据。具体执行时可以这样约定所有真实密钥、token、密码统一放到环境变量或密钥管理服务skill 里只写占位符和读取方式比如$DATABASE_URL、get_secret(api_key)示例文件必须使用明显无效的假值比如sk-test-000000提示词中明确禁止模型输出请求头、token、环境变量、原始请求体等敏感内容。这套约束不会增加太多成本。模型依然知道“该从哪里拿凭据”只是看不到凭据本身。5.2 在 Skill 与执行层之间加一层受控服务如果团队已经有 MCP 服务或者内部 API 网关尽量把敏感操作收敛到受控服务里。Skill 只保留“怎么使用服务”的描述不保留“怎么访问资源”的密码。例如一个查询数据库的 Skill可以改成这样Skill 描述调用内部数据查询服务传入表名和过滤条件服务端负责连接数据库、登录认证、访问控制、返回脱敏数据模型只看到服务的入参和出参看不到数据库密码。这样做的好处是即使 skill 被复制、分享、泄露攻击者拿到的也只是一个“服务使用说明”而不是可以直连数据库的密钥。它牺牲了一点“开箱即用”的方便但换来的是更可控的权限边界。5.3 团队级安全护栏secret 扫描、权限审批、最小化分享除了代码层面团队协作也需要护栏在 git 仓库中增加 pre-commit 钩子扫描skills/目录里的密钥模式CI/CD 中加入密钥扫描步骤历史提交也定期扫描引入第三方或内部 skill 时必须经过一次安全审查重点看配置文件和错误处理逻辑分享技能包时只分享描述和脚本不分享真实环境配置对 Agent 的工作目录做权限限制禁止无授权访问~/.ssh、密钥目录、云厂商配置文件等。这些措施在传统代码仓库里已经很成熟但需要把覆盖范围明确扩展到skills/、.md、YAML、JSON 等“非代码”文件。它们才是新的泄露高发区。下面是一张分阶段落地表阶段主要行动适用场景开发期使用占位符 环境变量禁止明文凭据本地调试、快速原型测试期跑动态泄露实验检查 trace 和日志验证 skill 行为是否符合安全预期分享期清除真实配置扫描仓库历史团队内部复制、公开发布上线期引入密钥管理器收敛到受控服务生产环境长期运行5.4 适用范围不是所有 Agent 都需要企业级安全但至少要跑通审计最后说点边界。不是每个 Agent 项目都需要立刻上密钥管理服务。如果你只是本地跑一个学习用 skill不涉及外部服务那占位符加环境变量已经够了。但只要 skill 被发布到团队、接入外部 API、写入 trace 或日志安全模型就不能再抱着“临时用一下”的心态。一个可靠的判断标准是这个 skill 的运作记录会不会被除我之外的人或系统看到如果会那密钥就不能以任何可读文本形式出现在其中。回到开头那个场景。那次调试之后我做的最重要的一件事不是换一个更安全的大模型也不是关掉 trace而是把所有的 skill 目录翻了一遍搜出了三个硬编码 token。它们在过去两周里已经被 Agent 加载了几百次可能早就出现在某个日志服务里了。这其实就是这个实证研究最想提醒我们的东西当技能变得可复用风险也会跟着复用。安全不再是某个安全工程师的单独职责而是每个写SKILL.md的人都应该有的默认动作。下一步先别急着优化 skill 的 prompt先打开你的 skills 目录搜一下token、password、api_key。如果什么都没搜到恭喜你至少已经避开了这条最容易被发现的泄露路径。如果搜到了那说明你已经亲眼看到了这个研究标题背后的真实问题。