十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLM Agent Skills凭据泄露风险分析:从机制到防护实践

LLM Agent Skills凭据泄露风险分析:从机制到防护实践 LLM Agent Skills 正在成为 Agent 应用里复用能力的主要方式但随之而来的凭据Credentials泄露风险往往被开发者低估。一篇题为《Credentials Are Leaked by LLM Agent Skills: An Empirical Study》的研究把这个问题摆到台面上API Key、数据库口令、云厂商访问密钥、OAuth Token都有可能在 Skill 的输出、日志、错误信息或仓库文件中被意外暴露。下面这篇文章会结合 Agent Skills 的运行机制先解释凭据为什么容易在这里泄露再提供一个可本地复现的最小实验最后给出检测、排查和生产环境落地的防护建议。无论你是 Agent 应用开发者、LLM 框架使用者还是负责平台安全的技术人员都可以按这套方法梳理自己项目里的风险面。需要先说明一点这篇文章不引用原论文的具体实验数据和结论只围绕“Credentials Are Leaked by LLM Agent Skills”这个事实方向搭建一套可自行验证的安全分析框架。所有实验都使用伪凭据不连接真实服务也不依赖外部模型厂商。1. Agent Skills 的凭据风险要从机制说起1.1 从“工具”到“技能”安全边界发生了转移在传统 Function Calling 或者插件模型里开发者会为模型注册一组函数{ name: query_order, description: 查询订单信息, parameters: { type: object, properties: { order_id: {type: string} }, required: [order_id] } }函数本身只是描述真正的业务逻辑由后端服务执行执行结果是否返回给模型、返回哪些字段还可以在代码里做二次控制。Agent Skills 则更进一步。一个 Skill 更像是一个可以独立执行的小任务单元通常由三部分组成一份描述文件比如skill.yaml说明这个 Skill 是干什么的、需要哪些环境变量、执行什么命令一段可执行脚本或命令比如 Python、Shell一个输出结果执行后的 stdout、stderr 或者返回 JSON 会被组装回 Agent 的上下文。这种设计让模型可以“看懂”技能并动态决定是否调用但安全边界也随之转移。普通脚本是开发者写死调用链路的输出数据不会自动进入模型上下文而 Agent Skill 的调用决策交给模型执行产生的原始输出又常常原样拼进上下文。两边叠加凭据暴露的窗口比静态脚本大很多。下表整理了几个关键差异。对比维度传统 Function CallingAgent Skills能力载体函数、后端接口脚本、命令、描述文件谁决定调用开发者编排或模型按 schema 调用模型根据 description 自主选择输出流向可以结构化过滤往往是文本回传容易被透传凭据管理位置集中在后端服务分散在 Skill 目录、配置和环境变量里风险特征边界清晰容易收敛边界分散需要额外脱敏处理1.2 一次 Skill 调用的完整生命周期与凭据接触点要理解凭什么据会泄露先看一个 Agent Skill 从定义到执行的完整过程开发者编写 Skill 目录包含描述文件和执行脚本将 Skill 注册到 Agent 编排框架Agent 运行时加载 Skill 元数据把描述和参数说明提供给模型模型根据用户请求选择某个 Skill框架执行 Skill 的 command捕获 stdout 和 stderr执行结果被写入 Agent 对话上下文日志系统记录本次调用的元数据、入参和输出。凭据在这个流程里有几个潜在接触点第 1 步如果凭据写在脚本、.env、skill.yaml里源码和配置就是泄露源第 5 步脚本在调试日志或 stdout 里打印环境变量凭据会进入执行输出第 6 步输出中包含的连接串、Token 会进入模型上下文控制权离开应用层第 7 步中心化日志保存了完整 stdout变成第二个存储侧泄露点。这里最容易误解的是“只要凭据放在环境变量里就安全”。实际上环境变量只是不把秘密写死在源码里但如果程序在日志中打印os.environ、debug 时输出DB_PASSWORD或者异常信息里包含完整配置对象凭据依然会从输出通道泄露出去。1.3 和 n8n、Flowise 等编排平台的 credentials 差异在 n8n、Flowise 这类工作流平台中Credentials 通常有独立配置页用户把 API Key、Token 存到平台的凭据仓库节点只引用变量名。这种模式相对集中风险主要集中在平台本身的鉴权和存储。Agent Skills 不同。当前很多实现是“落盘”的一个 Skill 就是一个目录里面除了描述文件还有脚本、配置甚至.env。如果采用同样松散的凭据管理方式每个 Skill 目录都可能成为独立泄露源。这也是实证研究值得关注的原因当 Skill 数量多起来凭据不是集中在一个地方而是散落在几十上百个目录里检测和治理难度会显著上升。2. 凭据泄露的四种典型链路2.1 写入侧配置、源码和 .env 进入版本库最常见的一类泄露发生在代码被写下来的一瞬间。Skill 的目录天然是“自包含”的开发者为了方便会在脚本里硬编码密钥或者把DB_PASSWORD写进skill.yaml再或者在 Skill 目录下放一个.env。这类问题在普通项目里也有但 Skill 让问题更容易出现因为 Skill 的复制传播成本很低一个写好了数据库凭据的 Skill 可能被团队成员直接复制到另一个项目里而.gitignore未必覆盖每个 Skill 目录。还有一种情况是环境变量文件没有被忽略或者被 CI 过程作为产物上传。此时凭据不仅进入源码仓库还可能进入构建日志、镜像层、制品仓库。后续清理成本非常高。2.2 执行侧debug 日志、stdout 和进程信息Agent 框架执行 Skill 时通常会用子进程方式运行命令并捕获输出。看一个常见实现import subprocess def run_skill(command: str): output subprocess.check_output( command, shellTrue, textTrue, stderrsubprocess.STDOUT ) return output这个代码本身没有把凭据写进日志但只要 Skill 脚本中写了类似logging.debug(connect db user%s password%s, user, password)那么password就会通过 stderr 被 subprocess 捕获。再由框架把这个输出组织成结果交给模型甚至写入日志。shellTrue还会带来另一条泄露路径当命令字符串包含动态拼接的参数时整个命令行可能出现在操作系统的进程列表或者 shell 历史里。如果拼接内容里有 Token凭据就等于落到了进程监控工具和管理员日志里。2.3 模型侧输出作为模型上下文边界不可控Agent Skill 的价值在于模型能理解执行结果并决定下一步动作。因此框架往往会尽量把 Skill 的 stdout 完整传给模型而不是只传一个状态码。问题就在这里。如果脚本返回了这样的结果{ status: success, connection: { host: 127.0.0.1, user: demo_user, password: FakePass123! } }这段文本会进入模型上下文。一旦进入模型服务方或者中转链路应用所在方就失去了对这部分数据的完全控制。即使模型不会主动输出这个字段数据也已经越过应用边界进入了第三方可观测范围。这是 Agent Skills 特有的风险因为普通函数调用通常可以设计“只返回必需字段”而 Skill 输出往往是原始文本。这里要强调一点不要把希望寄托在“模型不会乱说”上。安全边界应该设计在输出形成之前而不是依赖模型对数据的自动隐藏。2.4 异常侧堆栈和回退信息包含连接串很多开发者在异常处理里习惯“把完整错误信息返回给模型让模型继续尝试”。例如except Exception as exc: return f执行失败完整错误{str(exc)}如果str(exc)里包含数据库连接字符串、配置对象、环境变量字典异常消息就会带走凭据。例如配置对象转字符串后可能是这样Config(host127.0.0.1, userdemo_user, passwordFakePass123!, databasedemo_db)这类内容进入模型上下文后日志和追踪系统也会同步记录。异常回退是一个非常隐蔽的通道因为它不是业务输出只在任务失败时出现但恰恰最容易携带调试信息。2.5 泄露链路横向对比泄露链路发生阶段典型凭据主要通道源码写入Skill 开发时硬编码密钥、.env版本库、镜像、制品执行输出脚本运行时密码、Token、连接串stdout、stderr、进程列表模型上下文结果回传脱敏缺失的返回体LLM 请求内容异常回退任务失败时配置对象、异常堆栈错误消息、日志3. 复现研究思路搭一个最小可运行的凭据泄露实验原论文的研究方式是实证分析那我们也用实验思路来建立直觉。下面这个实验不连真实数据库、不调用真实模型只模拟“Agent Runner 执行 Skill 并捕获输出”这个过程。所有密码、Token 都是伪凭据即使泄露也不影响任何真实服务。3.1 实验目标、安全边界和环境准备实验目标有三个观察坏写的 Skill 如何把凭据带到 stdout观察 debug 日志如何把凭据带进 stderr用一个静态扫描脚本找出这些疑似凭据点。环境要求很低项目要求说明Python3.9 及以上仅使用标准库操作系统Linux / macOS / Windows示例命令以 Linux 风格展示外部依赖无不需要安装第三方包真实凭据不使用全部使用占位值建议准备一个独立目录agent-skills-security-lab实验结束可以直接删除。不要把这个实验放在真实项目的上层目录里运行避免扫描出真实环境的敏感信息后误处理。3.2 准备三个 Skill 样本目录结构如下agent-skills-security-lab/ ├── detector.py ├── runner.py ├── skills/ │ ├── .env.example │ ├── bad_hardcode/ │ │ ├── skill.yaml │ │ └── run.py │ ├── debug_output/ │ │ ├── skill.yaml │ │ └── run.py │ └── safe_skill/ │ ├── skill.yaml │ └── run.py先看坏示例一把密码放进返回结果里。# skills/bad_hardcode/run.py import json # 错误示范为了说明问题把凭据以调试字段带出 DB_PASSWORD FakePass123! DB_USER demo_user host 127.0.0.1 result { status: success, connection: { host: host, user: DB_USER, password: DB_PASSWORD } } print(json.dumps(result, ensure_asciiFalse))它的技能描述文件# skills/bad_hardcode/skill.yaml name: bad_hardcode description: 查询演示数据库订单数量 type: command command: python run.py env: - DB_HOST - DB_USER - DB_PASSWORD坏示例二debug 日志输出环境变量。# skills/debug_output/run.py import os import logging logging.basicConfig(levellogging.DEBUG) DB_USER os.getenv(DB_USER, demo_user) DB_PASSWORD os.getenv(DB_PASSWORD, fallback_secret) # 错误示范debug 日志直接记录完整环境变量 logging.debug(开始连接数据库 user%s password%s, DB_USER, DB_PASSWORD) print(查询完成)安全示例不打印凭据只输出业务状态和会话信息。# skills/safe_skill/run.py import os import logging logging.basicConfig(levellogging.INFO) DB_USER os.getenv(DB_USER, ) # 演示从外部临时凭据源获取不在脚本里硬编码 token os.getenv(DB_TOKEN, ) # 记录时只保留脱敏信息不打印完整 token safe_token token[:4] **** if token else N/A logging.info(connect dbdemo_orders user%s token%s, DB_USER, safe_token) print(query_ok)再放一个常见的.env.example演示“配置文件即使只是模板也可能被扫描器识别”。# skills/.env.example DB_HOST127.0.0.1 DB_USERdemo_user DB_PASSWORDchange_me这里的关键点是safe_skill仍然打印了tokenabcd****但只保留前 4 位并且整体是脱敏格式。实际生产里还可以更保守比如日志里只打印session_id完全不出现 token 前缀。3.3 用 Runner 模拟 Agent 调度过程下面写一个简化版 Runner遍历skills目录下的每个 Skill执行skill.yaml里声明的命令捕获 stdout 和 stderr。它模拟的是主流 Agent 框架“执行 Skill - 收集输出 - 返回给模型”的核心链路。# runner.py import os import subprocess from pathlib import Path BASE_DIR Path(__file__).parent SKILLS_DIR BASE_DIR / skills def run_skill(skill_dir: Path) - str: # 演示场景固定执行 python run.py # 真实项目应该解析 skill.yaml 中的 command 字段 command python run.py try: output subprocess.check_output( command, cwdskill_dir, shellTrue, textTrue, stderrsubprocess.STDOUT, ) return f[stdout]\n{output} except subprocess.CalledProcessError as exc: return f[failed]\n{exc.output} def main(): for skill_dir in sorted(SKILLS_DIR.iterdir()): if not skill_dir.is_dir(): continue if not (skill_dir / skill.yaml).exists(): continue print(f Skill: {skill_dir.name} ) print(run_skill(skill_dir)) print() if __name__ __main__: main()这里用shellTrue是为了模拟很多真实项目里的写法和风险。生产环境不建议这样做尤其是当 command 字符串包含外部输入或环境变量时应当改用参数列表方式调用避免命令注入和进程列表泄露。3.4 运行实验查看泄露现象在项目根目录执行cd agent-skills-security-lab python runner.py预期输出如下 Skill: bad_hardcode [stdout] {status: success, connection: {host: 127.0.0.1, user: demo_user, password: FakePass123!}} Skill: debug_output [stdout] DEBUG:root:开始连接数据库 userdemo_user passwordfallback_secret 查询完成 Skill: safe_skill [stdout] INFO:root:connect dbdemo_orders userdemo_user tokenN/A query_ok可以看到前两个 Skill 都出现了明显的凭据泄露bad_hardcode把password放进了返回 JSONdebug_output把password打印到了日志输出safe_skill只暴露了脱敏后的 token 前缀风险可控。如果这个实验接入真实模型bad_hardcode和debug_output的整段输出都会进入模型上下文。即使模型本身不记录这些内容凭据也已经通过了应用层边界。3.5 用 detector.py 做静态扫描静态扫描不能替代人工审计但可以快速发现已知模式。下面写一个启发式扫描器识别password、api_key、secret、token、云访问密钥和私钥片段。# detector.py import re import sys from pathlib import Path SECRET_PATTERNS [ (re.compile(r(?i)(password|passwd|pwd)\s*[:]\s*\S), password), (re.compile(r(?i)(api[_-]?key|secret|token)\s*[:]\s*\S), api key/secret/token), (re.compile(r(?i)(AKIA[0-9A-Z]{16})), cloud access key sample), (re.compile(r(?i)(BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY)), private key), ] EXCLUDE {.git, __pycache__, .venv, node_modules} def scan_file(path: Path): findings [] try: text path.read_text(encodingutf-8, errorsignore) except Exception: return findings for lineno, line in enumerate(text.splitlines(), 1): for pattern, name in SECRET_PATTERNS: if pattern.search(line): findings.append((lineno, name, line.strip())) return findings def main(root: Path): total 0 for path in root.rglob(*): if path.is_dir(): continue if any(part in EXCLUDE for part in path.parts): continue if path.suffix .pyc: continue findings scan_file(path) for lineno, name, line in findings: total 1 print(f{path}:{lineno} [{name}]) print(f - {line[:160]}) print(f\n扫描完成共发现 {total} 处疑似凭据点。) if __name__ __main__: main(Path(sys.argv[1]) if len(sys.argv) 1 else Path(__file__).parent)执行扫描python detector.py .预期输出类似skills/.env.example:3 [password] - DB_PASSWORDchange_me skills/bad_hardcode/run.py:9 [password] - result {status: success, connection: {host: host, user: DB_USER, password: DB_PASSWORD}} skills/debug_output/run.py:10 [password] - logging.debug(开始连接数据库 user%s password%s, DB_USER, DB_PASSWORD) 扫描完成共发现 3 处疑似凭据点。从这个结果能看出两个重要事实静态扫描可以发现源码和配置里的凭据点但未必能发现运行期动态拼出的凭据即使扫描到password也需要人工判断是真实泄露还是字符串字面量误报。所以扫描是第一步运行期检测和日志审查才是完整闭环。注意实验中的所有伪凭据只用于本地演示。不要把.env.example、FakePass123!、change_me直接当成安全示例复制到生产项目。4. 输出侧排查在凭据进入模型上下文之前拦下来4.1 先判断泄露发生在哪一个接触点拿到一份泄露报告时不要急着改日志先判断泄露点泄露现象大概率发生位置排查方向Git 历史里有密码写入侧查.gitignore、历史提交、镜像层运行时 stdout 有 Token执行侧查脚本 print、json dumps、调试输出日志系统出现凭据日志侧查 logging 配置、日志采集管道模型输出里出现凭据上下文侧查传给模型的完整结果、异常消息进程列表出现 Token命令执行侧查shellTrue、字符串拼接命令排查顺序建议是先看输出再看日志再查源码写入最后检查模型上下文。输出不泄露日志通常也不存在源码和配置是源头必须同时收敛。4.2 输出侧拦截设计统一的脱敏层Agent 框架里比较有效的做法是在“Skill 输出”进入模型上下文之前加一层统一脱敏函数。它的职责是扫描所有要回传给模型的文本把高风险字段替换成掩码。import re # 统一脱敏规则匹配常见凭据字段 REDACT_PATTERNS [ re.compile(r(?i)(password|passwd|pwd)\s*[:]\s*\S), re.compile(r(?i)(token|api[_-]?key|secret)\s*[:]\s*\S), re.compile(r(?i)(Bearer\s)[A-Za-z0-9._~/-]*), ] def redact_output(text: str) - str: result text for pattern in REDACT_PATTERNS: result pattern.sub(r\1***, result) return result使用时机很关键脱敏层不能放在 Skill 脚本内部因为每个 Skills 写法都不一样应该放在框架层作为所有 Skill 输出的统一出口。这样即使某个 Skill 写得不规范框架也能兜底。脱敏层也会带来副作用内容被掩码后模型可能无法基于真实字段继续完成任务。因此更合理的做法是先在 Skill 规范里约定“任何凭据字段不得出现在返回值中”脱敏层只是最后一道防线。4.3 日志与追踪链路排查日志系统是凭据的第二个“记忆仓库”。即使 stdout 没有泄露只要日志采集器把 stderr 和完整堆栈写入 Elasticsearch、Loki 或云日志服务凭据就会长期保存。排查日志链路时重点看三处logging.basicConfig(levellogging.DEBUG)项目里是否存在过宽的日志级别是否把os.environ、配置对象、异常对象直接str()后写入日志日志采集器是否自动记录环境变量或 HTTP Header。如果使用的是开源日志协议比如 OpenTelemetry要关注 Span Attribute 里是否填充了敏感字段。很多默认采集不会自动脱敏需要显式配置密钥过滤规则。4.4 常见坑与处理建议常见坑为什么危险处理建议.env文件被提交到 Git凭据进入版本历史和分发渠道删除历史、轮换凭据、完善.gitignoredebug 日志打印os.environ环境变量字典包含全部密钥只打印变量名不打印值Skill 返回 JSON 带调试字段输出会直接进入模型上下文返回前白名单过滤字段异常处理返回str(exc)连接串、配置对象可能被包含返回错误码和摘要屏蔽敏感字段shellTrue拼接命令行命令字符串进进程列表和 shell 历史使用参数列表方式调用集中式日志未脱敏凭据被持久化保存日志采集前做掩码处理5. 生产环境防护落地5.1 凭据注入方式从“跟随 Skill”改为“运行期注入”最核心的迁移目标是让 Skill 目录里不出现任何凭据。Skill 打包、分发、复制时目录里只有代码和描述文件没有.env没有硬编码密钥。运行期注入的方案可以按项目复杂度选择方案适用场景优点注意点进程环境变量小型项目简单直接不许打印不许写日志密钥管理服务中大型项目集中存储、审计需要网络权限和 SDKOIDC 短期凭据云原生容器或 CI无长期密钥自动轮换需要平台支持和角色配置如果暂时无法迁移到密钥管理服务也应该立即执行两条规则Skill 目录不允许存在.env文件所有脚本只允许从外部注入的环境变量读取密钥不允许在代码里写默认密钥。5.2 最小权限和短期凭据给每个 Skill 分配独立身份而不是让所有 Skill 共用一个高权限账号。独立身份的意义在于一个 Skill 泄露凭据只影响它对应的函数、表和 API 范围不会波及其他能力。配合短期凭据使用效果更好。例如数据库可以使用临时密码云资源可以用 OIDC 换取短期 Token。短期凭据过期后即使进入日志价值也会大大降低。5.3 生命周期治理写前扫描、运行监控、日志脱敏凭据治理应该是持续过程而不是上线前的一次性检查写代码阶段IDE 插件或 pre-commit 钩子扫描新增代码提交阶段CI 里跑 gitleaks、trufflehog 等工具运行阶段框架层统一脱敏输出日志阶段日志采集后执行敏感字段过滤和告警响应阶段发现泄露后立即轮换凭据并调查泄露范围。考虑到工具版本和项目环境的差异这里不展开某个工具的安装步骤落地前要以当前项目使用的版本和文档为准。核心思路是“每一层都设一道闸门”不能只靠开发者自觉。5.4 学习环境、测试环境、生产环境差异维度学习环境测试环境生产环境凭据来源可写死或使用占位值独立测试库最小权限密钥管理服务或短期 Token日志级别DEBUG 可开INFO/WARN脱敏后查强制脱敏禁止打印凭据模型调用可控沙箱使用伪数据严格限制返回内容统一脱敏层兜底泄露响应删除实验目录即可轮换测试凭据事件响应撤回历史通知相关方注意不要用生产凭据在测试环境里跑实验。测试环境一旦接入真实密钥泄露影响并不比生产环境小多少。6. 团队可复用清单与后续方向6.1 上线前检查清单每次发布一个 Agent Skill 前至少检查以下项目Skill 目录里是否存在.env、密钥文件、证书、.pemskill.yaml中是否有password、token、api_key字段运行脚本是否把环境变量或配置对象输出到日志异常处理是否会把完整堆栈返回给模型框架层是否有输出脱敏函数日志采集系统是否配置了敏感字段过滤该 Skill 使用的凭据是否对应最小权限账号凭据是否设置了过期时间是否能在中心化日志系统里搜索到该 Skill 名称并确认没有敏感数据是否把实验目录排除在扫描范围之外。6.2 泄露事件的响应路径如果已经确认凭据泄露按以下顺序处理立即吊销或轮换泄露的凭据确认泄露范围源码仓库、日志系统、镜像、模型上下文、第三方服务检查泄露凭据的权限范围判断是否还需要撤销相关账号权限定位泄露源头修复写入或输出代码增加自动检测规则避免同类问题再次出现保留审计记录确认是否有异常访问。不要等到“定位到根因再轮换密钥”。凭据一旦进入模型上下文或日志系统就无法确认谁看到过第一时间轮换是止损的前提。6.3 如何持续验证防护效果持续验证不能只靠代码 review建议周期性执行三件事随机抽取部分 Skill运行实验脚本并抓取 stdout、stderr、返回给模型的文本检查是否出现占位密文在测试环境用一组“伪泄露样例”验证检测规则是否有效定期扫描 Git 历史确认历史提交中不会重新出现凭据。如果团队引入了观测系统还可以关注三类指标隐藏凭据检测命中数、日志脱敏覆盖率、密钥轮换频率。检测命中数快速下降不一定代表绝对安全但持续为零且覆盖范围明确至少说明基础面被控制住了。Agent Skills 给自动化和 LLM 应用带来了很灵活的封装方式但这份自由也意味着输出不能被当作可信数据直接消费。凭据泄露并不是单个开发者编码习惯差造成的而是 Skill 这种封装单元天然把命令、配置和输出绑在了一起。设计阶段就让 Skill 不持有凭据运行时从外部获取最小权限的短期身份输出前用统一脱敏层拦截存储侧坚持扫描仓库、日志和 CIAgent 的自动化程度越高凭据反而应该越安全。
返回列表