拓冰建站拓冰建站
首页 / 资讯中心 / 正文

LLM推理轨迹中的敏感信息泄露:从风险原理到Aileaks扫描器实战

先说明一个常见场景你在本地调试一个 RAG 或 Agent 应用为了让问题更直观随手把system_prompt、tool_calls、raw_response一整套打印到了日志甚至临时写进了一个trace.json。代码跑通后你把这个文件连同项目一起提交到了仓库。问题往往就是这么发生的——LLM 推理轨迹reasoning trace里的敏感信息就这样进了代码库。本文以“Aileaks”这类专门扫描代码仓库中泄露的 LLM reasoning-trace secrets 的工具为切入点先讲清楚 reasoning trace 为什么危险、扫描器应该如何设计然后提供一个可用 Python 独立运行的最小扫描实现最后整理常见的误报场景和工程化落地方案。适合正在做 LLM 应用开发、接触 Agent / RAG 项目或者关注 AI 工程安全的开发者阅读。1. 背景与核心概念1.1 什么是 LLM reasoning-trace所谓 LLM reasoning-trace直译是“LLM 推理轨迹”指的是模型在产出最终答案之前系统记录下来的中间过程信息。这些信息在传统请求-响应链路里很少被暴露但在今天的大模型应用工程中却很常见。举个例子一个基于 RAG 的问答系统收到用户问题后会先做向量检索再把检索结果和系统提示词拼装成 prompt 发给大模型。为了排查“为什么答案不准”开发者在代码里加了日志把系统提示词模板用户原始输入检索到的文档片段拼装后的完整 prompt模型返回的原始输出包括 reasoning 字段一轮会话中的完整上下文对象。全部写入日志或 JSON 文件。这一串记录就是一条典型的 reasoning trace。在 Agent 应用里这类信息更复杂。一次工具调用会记录选择了哪个工具传给工具的 JSON 参数工具返回结果模型基于结果生成的下一步计划内部使用的会话标识、调用标识。在实际项目中这些内容经常被持久化成日志、JSONL 文件或者调试 dump。如果这些文件被提交到仓库就等于把整个内部推理过程公开了。1.2 为什么 reasoning-trace 中会有 secrets很多人会问模型推理过程里哪来的秘密这其实是工程链路的问题不是模型主动泄露而是上下文拼装时把敏感信息带了进去。常见情况有泄露来源具体场景风险说明Prompt 中的密钥为了快速调试直接把 API Key、Token 写死在系统提示词里任何人拿到提示词就等于拿到了密钥工具调用参数Agent 调用内部 API 时URL、Header、认证信息写进了 tool call 参数泄露内部服务地址和认证方式RAG 检索内容知识库中包含内部接口文档、数据库连接串或客户信息推理轨迹把这些文档全文暴露出来日志打印异常处理时把完整请求体打印到控制台再重定向到日志文件日志文件被提交到仓库后直接外泄上下文对象序列化调试时把整个 session_context 序列化保存会话标识、用户 ID、内部字段全部暴露这些情况在实际开发中出现的频率非常高。因为 LLM 应用的调试习惯和传统后端不太一样传统后端排查问题时主要看请求参数、数据库日志和异常栈而 LLM 应用排查问题时必须看完整 prompt、中间推理、工具调用结果。于是“为了排查方便而打印的东西最后被提交到了仓库”就成了一条新的泄露链路。1.3 Aileaks 解决什么问题Aileaks 从项目名可以看出它把“AILLM”和“Leaks泄露”组合在一起目标很明确扫描代码仓库找出与 LLM 推理轨迹相关的敏感信息泄露。传统 secret 扫描工具不是不能做这件事比如 Gitleaks、TruffleHog 都可以扫描 API Key、私钥、高熵字符串。但它们的思路更偏“固定模式匹配”很难判断一段文本到底是不是存在于 LLM trace 上下文里。举个例子{reasoning_trace: need to call internal api, api_key: sk-xxx}如果只看sk-xxx普通扫描器会发现但很难告诉你“这是从 LLM 推理轨迹中泄露的”。Aileaks 这类工具的价值在于把“密钥特征”和“LLM 轨迹语义”结合起来给出更精准的判断。围绕这个目标至少需要具备这些能力遍历仓库工作区文件提取可读文本识别常见 LLM Provider API Key、私钥、高熵密钥通过文件路径、文件内容中的关键词判断是否属于 reasoning-trace 材料输出风险等级和匹配上下文方便人工复核支持报告导出便于接入 CI/CD 或安全运营。接下来我们用一个小而完整的项目来拆解这一切。2. 环境准备与版本说明2.1 基础环境本文的示例代码为了降低接入成本只使用 Python 标准库不依赖第三方包。建议环境如下操作系统Windows / macOS / Linux 均可Python 版本3.10 或更高版本示例代码用到了pathlib、argparse、json等模块这些在 3.10 中都很稳定命令行工具Git、Python 解释器测试对象一个本地代码目录或 Git 仓库。版本不需要太纠结。如果你本机是 Python 3.9大部分代码也能运行只需注意Path.rglob、str.removeprefix这类 API 在新旧版本之间的差异。本文示例不使用前沿语法重点演示配置思路。2.2 准备一个测试仓库我们先创建一个测试目录用来演示扫描效果mkdir -p /tmp/demo-llm-repo/app mkdir -p /tmp/demo-llm-repo/logs cd /tmp/demo-llm-repo git init创建几个模拟文件cat /tmp/demo-llm-repo/app/main.py EOF def call_llm(prompt): return {text: ok} EOF cat /tmp/demo-llm-repo/logs/trace_20250101.jsonl EOF {event:llm_response,reasoning_trace:user asks for refund, need to check policy,openai_key:sk-1234567890abcdef1234567890abcdef} EOF注意这里使用的是模拟密钥仅用于演示扫描效果请勿在任何真实环境中使用这类样例密钥。3. 核心原理拆解3.1 扫描对象特征设计要让扫描器判断“某个文件是否泄露了 LLM reasoning-trace secrets”首先要理解特征。我们可以把特征分为三类特征类型示例作用固定密钥模式sk-开头的高熵字符串、AKIA 开头的 AWS Key、PEM 私钥通用 secret 识别推理轨迹关键词reasoning_trace、chain_of_thought、tool_calls、system_prompt、raw_response判断是否处于 LLM 上下文文件路径提示文件名或目录包含trace、prompt、llm-log、session提升命中置信度需要注意这些规则并不是越严越好。如果规则太宽会把普通的 README 示例密钥误报成高优问题如果规则太窄又会漏掉真正的泄露文件。所以通常需要引入一个置信度评分机制。3.2 规则引擎从“匹配”到“判断”普通的正则匹配只能回答“文件里有没有疑似密钥”。而 Aileaks 这类工具需要进一步回答“这个密钥是否和 LLM 推理轨迹有关”。为此规则引擎可以设计成先用密钥正则表达式扫描文件文本如果命中再对文件路径和文件内容进行语义加分根据总分计算置信度在报告里保留匹配位置前后的上下文片段方便人工确认。举个例子文件app/config.py里写了一条sk-xxx可能是真实配置被误提交文件logs/llm_trace.jsonl里出现reasoning_trace和sk-xxx几乎可以断定是调试日志泄露。两者的初始命中相同但后者因为有 trace 关键词和路径提示置信度会明显更高。3.3 扫描工作流一个完整的本地扫描工作流可以设计成下面这样仓库根目录 - 遍历所有文件 - 跳过 .git、node_modules、二进制文件、超大文件 - 读取文本内容 - 密钥正则匹配 - 命中后计算上下文评分 - 输出结构化报告如果放在 CI 中还可以在生成报告后设置退出码无高危问题时退出码为 0有高危问题时退出码非 0让流水线据此决定是否阻断合并。4. 完整实战案例实现一个最小 Aileaks Scanner这一节我们用 Python 实现一个可运行的最小扫描器。项目结构如下aileaks_demo/ ├── scanner.py ├── rules.json └── report.json4.1 项目结构说明scanner.py扫描器主程序。rules.json规则配置文件可以独立调整。report.json扫描结果输出文件。这样设计的好处是规则和代码分离。新增一种密钥类型时不需要改代码只改 JSON。4.2 规则配置文件{ secret_patterns: [ { name: OpenAI API Key, regex: sk-[A-Za-z0-9_-]{20,}, weight: 100 }, { name: Anthropic API Key, regex: sk-ant-[A-Za-z0-9_-]{20,}, weight: 100 }, { name: AWS Access Key ID, regex: AKIA[0-9A-Z]{16}, weight: 80 }, { name: Private Key, regex: -----BEGIN [A-Z ]*PRIVATE KEY-----, weight: 100 } ], trace_keywords: [ reasoning_trace, chain_of_thought, tool_calls, system_prompt, raw_response, invocation_id ], trace_path_hints: [ trace, tracing, prompt, llm-log, llm_log, session, agent, reasoning ] }这里有几个设计点weight表示密钥本身的危险程度权重越高越有可能被判定为高风险trace_keywords是 file content 中的语义关键词trace_path_hints是路径中出现的提示词用于给疑似轨迹文件加分。4.3 核心扫描代码#!/usr/bin/env python3 # -*- coding: utf-8 -*- Aileaks Demo Scanner 扫描本地代码仓库识别可能泄露 LLM reasoning-trace secrets 的文件。 import argparse import json import re from pathlib import Path DEFAULT_RULES { secret_patterns: [ {name: OpenAI API Key, regex: rsk-[A-Za-z0-9_-]{20,}, weight: 100}, {name: Anthropic API Key, regex: rsk-ant-[A-Za-z0-9_-]{20,}, weight: 100}, {name: AWS Access Key ID, regex: rAKIA[0-9A-Z]{16}, weight: 80}, {name: Private Key, regex: r-----BEGIN [A-Z ]*PRIVATE KEY-----, weight: 100} ], trace_keywords: [ reasoning_trace, chain_of_thought, tool_calls, system_prompt, raw_response, invocation_id ], trace_path_hints: [ trace, tracing, prompt, llm-log, llm_log, session, agent, reasoning ] } IGNORED_DIRS { .git, node_modules, .venv, venv, dist, build, __pycache__, .idea, .vscode, .mypy_cache, .pytest_cache } IGNORED_SUFFIXES {.pyc, .png, .jpg, .jpeg, .gif, .pdf, .zip, .tar, .gz, .db, .sqlite} MAX_FILE_SIZE 2 * 1024 * 1024 def load_rules(rules_path): 加载规则配置如果文件不存在则使用内置默认规则。 if rules_path and Path(rules_path).exists(): with open(rules_path, r, encodingutf-8) as f: return json.load(f) return DEFAULT_RULES def is_ignored(path): 判断文件是否应被跳过。 return any(part in IGNORED_DIRS for part in path.parts) def is_binary(content): 通过空字节判断文件是否为二进制。 return b\x00 in content[:1024] def path_score(path, rules): 根据文件路径中的关键词加分。 path_lower path.lower() score 0 for hint in rules.get(trace_path_hints, []): if hint in path_lower: score 20 return score def content_score(text, rules): 根据文件内容中出现的 reasoning-trace 关键词加分。 text_lower text.lower() score 0 for keyword in rules.get(trace_keywords, []): if keyword in text_lower: score 10 return score def scan_file(path, rules): 扫描单个文件返回命中结果列表。 results [] try: size path.stat().st_size if size MAX_FILE_SIZE: return results raw path.read_bytes() except (PermissionError, OSError): return results if is_binary(raw): return results text raw.decode(utf-8, errorsignore) base_score path_score(str(path), rules) content_score(text, rules) for pattern in rules.get(secret_patterns, []): try: regex re.compile(pattern[regex], re.IGNORECASE) except re.error: continue for match in regex.finditer(text): snippet_start max(0, match.start() - 80) snippet_end min(len(text), match.end() 80) context text[snippet_start:snippet_end].replace(\n, ) match_text match.group(0) short_match match_text[:16] ... if len(match_text) 16 else match_text confidence min(99, pattern[weight] base_score) results.append({ file: str(path), rule: pattern[name], match: short_match, confidence: confidence, context: context.strip() }) return results def scan_repo(root, rules): 遍历仓库目录汇总所有命中结果。 findings [] if not root.exists(): print([!] 路径不存在:, root) return findings for path in root.rglob(*): if not path.is_file(): continue if is_ignored(path): continue if path.suffix in IGNORED_SUFFIXES: continue findings.extend(scan_file(path, rules)) return findings def generate_report(findings, output_path): 生成报告并输出 JSON 文件。 report { total: len(findings), issues: findings } if output_path: with open(output_path, w, encodingutf-8) as f: json.dump(report, f, ensure_asciiFalse, indent2) return report def main(): parser argparse.ArgumentParser(descriptionAileaks Demo Scanner) parser.add_argument(--repo, requiredTrue, help要扫描的仓库目录或项目目录) parser.add_argument(--rules, help规则配置文件路径默认使用内置规则) parser.add_argument(--output, help扫描报告输出路径) args parser.parse_args() rules load_rules(args.rules) print([*] 开始扫描:, args.repo) findings scan_repo(Path(args.repo), rules) findings.sort(keylambda x: x[confidence], reverseTrue) report generate_report(findings, args.output) print([*] 扫描完成共发现, report[total], 个问题) for item in findings: print(f - [{item[confidence]:3d}] {item[rule]} - {item[file]}) print(f context: {item[context]}) if args.output: print([*] 报告已保存到:, args.output) if __name__ __main__: main()代码关键在于path_score和content_score分别从路径、内容两个维度给文件加分confidence是密钥权重与上下文得分的组合最高 99报告保留context方便人工看到命中位置附近的文本。4.4 运行与验证把scanner.py和rules.json放到同一个目录然后运行cd /tmp/aileaks_demo python scanner.py --repo /tmp/demo-llm-repo --output report.json如果仓库中确实存在我们构造的测试文件预期输出类似[*] 开始扫描: /tmp/demo-llm-repo [*] 扫描完成共发现 1 个问题 - [130] OpenAI API Key - /tmp/demo-llm-repo/logs/trace_20250101.jsonl context: {event:llm_response,reasoning_trace:user asks for refund...,openai_key:sk-1234567890abcdef1234567890abcdef} [*] 报告已保存到: report.json可以看到这个文件因为同时命中OpenAI API Key 正则内容里的reasoning_trace关键词路径中的trace提示词。所以置信度很高最终判定为一次 LLM reasoning-trace 密钥泄露。4.5 结果说明生成的report.json结构如下{ total: 1, issues: [ { file: /tmp/demo-llm-repo/logs/trace_20250101.jsonl, rule: OpenAI API Key, match: sk-1234567890abcdef..., confidence: 130, context: {\event\:\llm_response\,\reasoning_trace\:\user asks for refund...\,\openai_key\:\sk-1234567890abcdef1234567890abcdef\} } ] }在真实项目中你会看到成千上万条扫描结果。到这一步最需要关注的是confidence排序靠前、且文件路径符合 trace/llm-log/session 特征的结果。这些基本可以确定是真正的泄露点。5. 常见问题与排查思路实际使用这类扫描器时一定会遇到下面这些情况。问题现象常见原因解决思路误报太多没有区分测试样例和真实密钥增加白名单、检测示例密钥前缀、提高置信度阈值漏掉压缩包中的文件只扫描文本文件忽略了 zip/tar 包在 CI 中先解压再扫描或集成压缩包解压逻辑扫描大仓库非常慢全量遍历所有文件包括无关目录增加忽略目录、限制文件大小、用多进程提升速度报告结果太多没法看没有按置信度排序先看高置信度再按规则维度分组只扫到当前快照没有扫描 git 历史额外实现git log -p或git clone全历史扫描中文或其他编码文件乱码decode(utf-8)无法处理部分编码读取时检测 BOM失败后尝试gbk、utf-16等二进制 key 匹配不到密钥被放在图片、PDF 中文本扫描无法覆盖需要 OCR 或人工审计其中误报和扫描速度是实际落地时最需要优先解决的问题。误报的常见来源是测试仓库。很多项目会在example、tests目录里放伪造密钥用来演示功能。为了防止这类误报可以为伪造前缀建一个白名单例如sk-1234、sk-test之类。也可以在规则层做字段区分只有weight高、且同时命中 trace 语义关键词的结果才进入“高风险”列表。扫描速度问题建议使用多进程。Python 的concurrent.futures.ProcessPoolExecutor可以非常容易地把单文件扫描任务分发到多个 CPU 核心。不过在分发前一定要保证每个扫描任务只读文件不修改任何状态否则容易出现结果错乱。6. 最佳实践与工程建议6.1 不要把密钥写进 Prompt这是最容易忽视的问题。很多开发者在调 RAG 时直接在系统提示词里写上You can access internal API using token: sk-xxx这会让模型每次调用都携带这个 token而所有 reasoning-trace 都会把它记录下来。正确做法是密钥通过环境变量或 Secret Manager 注入系统提示词中只写“调用内部工具时使用认证服务”不直接写具体 Token需要传给 Agent 工具的参数在函数内部读取环境变量而不是拼在 prompt 里。6.2 日志与追踪系统必须脱敏无论是本地日志还是接入 OpenTelemetry、Langfuse、LangSmith 这类追踪平台都必须对敏感字段做脱敏。最简单的脱敏规则可以直接在日志输出层处理def mask_secret(value: str) - str: if not value: return value if len(value) 8: return *** return value[:4] **** value[-4:]在记录tool_calls、prompt、response时统一通过mask_secret处理 Token、Key、密码等字段。6.3 发现泄露后的响应流程如果扫描器真的在仓库里发现了泄露密钥不要只删掉文件就完事。推荐按以下步骤处理确认泄露范围包括哪些文件、哪些分支、哪些历史提交立即吊销对应密钥从工作区删除文件并重写 Git 历史具体可使用git filter-branch或git-filter-repo检查密钥对应服务的访问日志确认是否已被恶意使用通知相关协作方和安全负责人将本次泄露特征补充到扫描规则中避免再次出现同类文件复盘问题根源优化日志脱敏方案。注意在真实生产环境中修改 Git 历史属于高风险操作尤其是团队协作仓库。执行前必须确认授权、备份完整并提前通知团队成员。6.4 接入 CI/CD扫描器不能只在本地跑。更合理的做法是让它在每次 push、pull request 时自动运行并只扫描增量文件。下面给出一段 GitHub Actions 思路具体版本需要根据你仓库实际情况调整name: llm-secrets-scan on: [push, pull_request] jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.12 - run: python scanner.py --repo . --output report.json - run: | python - EOF import json report json.load(open(report.json)) high_risk [i for i in report[issues] if i[confidence] 100] if high_risk: print(存在高风险泄露阻止合并) exit(1) EOF这套方案并不复杂核心是扫描结果要成为门禁而不是一份没人看的报告。7. 总结与学习路线这篇文章从 LLM reasoning-trace 泄露的风险出发围绕 Aileaks 这类“仓库推理轨迹密钥扫描工具”的设计思路完整拆解了以下关键点reasoning-trace 是什么为什么里面会混入密钥扫描器可以基于哪些特征判断泄露如何用 Python 实现一个最小可运行的扫描器常见误报、漏报和性能问题如何处理如何从密钥治理、日志脱敏、CI/CD 门禁等角度把工具落地到工程体系。如果你是在校学生可以通过这个例子理解“安全扫描”和“代码仓库”之间的关系如果你是后端或 AI 应用开发者我建议从这一步开始把你项目里的日志目录和 prompt 模板目录单独建一个扫描规则先用最小脚本跑一遍看看能发现多少意外惊喜。下一步可以继续拓展的方向包括学习 Gitleaks、TruffleHog 这类成熟 secret 扫描工具的实现思路深入研究 Agent 安全中的工具权限控制、MCP 网关认证把扫描能力集成进 Git 服务端 pre-receive 钩子形成更前置的防线结合大模型的输出审计设计更完善的 LLM 应用日志脱敏规范。安全问题的本质不是“用了什么工具”而是“你是否有意识地审计每一条进入仓库的数据”。写完这个最小扫描器之后建议马上把它用在自己的真实项目上不需要太多代码跑一次可能就有收获。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门