首页
/ claude-mem 安全加固实战:watch.context.path 路径穿越防护、多用户端口隔离与 CI 注入审计

claude-mem 安全加固实战:watch.context.path 路径穿越防护、多用户端口隔离与 CI 注入审计

2026-09-06 19:17:58作者:裴锟轩Denise

本文围绕 claude-mem 仓库中的 Issue 分诊 Playbook TRIAGE-05-Security-Fixes.md(分诊第 05 阶段:高优先级安全修复)展开。该阶段对应 Issue #1204、#1255、#1285、#1251 四个安全类问题,涵盖 transcript watch 配置引发的任意文件写入、macOS 多用户环境下的端口共享数据串扰、GitHub Actions 工作流注入审查以及整体安全审计响应。读完本文,你可以掌握该项目的安全威胁模型:用户可控路径如何进入写文件调用链、worker 本地端口的多用户边界问题,以及如何验证 CI 工作流不存在 ${{ github.event.* }} 注入面,并对照当前仓库源码确认各项修复的实际落地形态。

一、阶段背景:安全类 Issue 的分诊结论

该阶段在 TRIAGE-05-Security-Fixes.md 中对四类问题给出了总体判定,这也是全文的核心脉络:

  • 一个真实漏洞:通过 watch.context.path 实现的任意文件写入(Issue #1204),代码分析确认 src/services/transcripts/processor.ts 会将用户配置文件中的路径直接用于写操作,且 expandHomePath() 只做 ~ 展开、不做边界校验;
  • 一个设计层面的隐患:多用户机器上的端口共享(Issue #1255),worker 绑定在 127.0.0.1 的固定端口,同一台 macOS 上的多个本地用户会共享同一端口,导致数据互相串扰;
  • 两个需要审计而非改代码的项:GitHub Actions 注入疑虑(Issue #1285)与安全审计请求(Issue #1251),代码分析确认 6 个工作流文件均无可利用的注入向量;
  • 前置条件:分诊阶段 01–04(PR 合并、进程/资源稳定性、Hook 会话生命周期、worker 服务可靠性)应先完成。

其中两项代码修复(#1204、#1255)在 Playbook 中已标记完成(分别新增 11 个和 9 个测试),后两项是审计文档类工作。下面逐项展开,并结合当前仓库源码核对修复的实际落地位置。

二、任意文件写入:watch.context.path 的边界校验(#1204)

攻击面在哪里

claude-mem 支持通过 transcript watch 配置监听各 Agent 的会话记录文件。watch 项中的 context.mode === 'agents' 表示:每当转录内容更新,就把压缩后的记忆上下文回写到 watch.context.path 指定的 AGENTS.md 类文件。这个路径来自用户主目录下的配置文件,而处理入口是 processor.ts 中的 updateContext()

问题链条很清晰:

  1. 配置加载函数 loadTranscriptWatchConfig() 读取并解析 JSON 配置,原实现中 context.path 字段不做任何路径合法性校验;
  2. 路径展开函数 expandHomePath()src/services/transcripts/config.ts 第 56–62 行)只处理一个职责——把 ~ 前缀替换为用户主目录,对 ../ 之类的穿越序列不做防御;
  3. 于是 processor 拿到 watch.context.path 后直接用于写入,攻击者(或误配置)可以把上下文内容写到系统上任意可写位置。

Playbook 中的修复设计

Playbook 给出的修复要点是「在写入前做边界校验,同时保持合理路径可用」:

  1. const resolvedPath = path.resolve(agentsPath) —— 先解析为绝对路径;
  2. const homeDir = os.homedir() —— 取用户主目录作为安全边界;
  3. 若解析后的路径不以 homeDir 开头,记录警告并提前返回;
  4. 若解析后的路径仍含 .. 段,拒绝(纵深防御);
  5. 同时在配置加载时(loadTranscriptWatchConfig())提前拒绝任何解析到主目录之外的 context.path
  6. 明确约束:校验不能过严,~/.codex/AGENTS.md~/project/AGENTS.md 这类合法路径必须保持可用。

当前源码中的落地形态

对照当前仓库代码,路径穿越防护实际落在两层:

第一层:processor 侧的根白名单校验。processor.tsupdateContext()(第 358–370 行)中,路径先经过 expandHomePath() 展开,再 path.resolve() 归一化,然后检查是否落在允许的根目录之内:

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;
}

从源码结构看,这里的边界策略相比 Playbook 初稿的「仅限用户主目录」更进一步:只允许写入当前会话工作目录claude-mem 数据目录(DATA_DIR) 之下,攻击面比整个 home 目录更小;且命中时以 SECURITY 分类打日志,保留 original/resolved/allowedRoots 三个字段便于审计,然后静默跳过而不是抛错中断 hook 链路。

第二层:写入函数自身的防御。 agents-md-utils.tswriteAgentsMd()(第 9–10 行)在写入前拦截 .git 路径:

const resolvedPath = resolve(agentsPath);
if (resolvedPath.includes('/.git/') || resolvedPath.includes('\\.git\\')
    || resolvedPath.endsWith('/.git') || resolvedPath.endsWith('\\.git')) return;

这层防护针对的是「把记忆上下文写进 .git/ 内部从而污染版本库」这一特定风险,覆盖 POSIX 与 Windows 两种分隔符形态。随后函数先写 ${agentsPath}.tmp 临时文件再 renameSync() 原子替换(第 24–28 行),避免半截写入损坏 AGENTS.md——这是 Playbook 未展开、但对「写入类修复」很重要的配套细节。

Playbook 同时要求不要过度收紧~/.codex/AGENTS.md 等路径必须继续合法。这一点在 config.tsisNativeHookBackedCodexWatch()(第 17–25 行)中得到印证——Codex 的 ~/.codex/sessions/**/*.jsonl watch 是产品支持的规范配置,白名单设计必须与之兼容。

三、多用户 macOS 下的跨账户数据泄漏:按 UID 派生端口(#1255)

问题本质

worker 是一个常驻本地 HTTP 服务,监听 127.0.0.1:端口。Playbook 指出的问题在于:原始默认端口是固定值 37777。在共享一台 macOS 机器(多个本地用户账户)的场景下,两个用户各自安装 claude-mem 后,两个 worker 会争抢/共享同一个端口——后启动的一方要么失败,要么连上别人的 worker,读取到他人的会话记忆数据,造成跨账户数据串扰。

Playbook 的修复方案

Playbook 给出的方案是按用户 UID 派生端口,核心规则:

  1. 获取当前用户 UID:process.getuid()(Unix)或 os.userInfo().uid(跨平台);
  2. 计算端口:37777 + (uid % 1000),让每个用户落在 1000 个端口的区间内互不冲突;
  3. 仅当用户没有显式设置 CLAUDE_MEM_WORKER_PORT才应用派生逻辑——环境变量优先级始终最高;
  4. 文档注释中的 37777 需要同步更新,但设置默认值本身保持 '37777' 作为基座,派生逻辑单独一层;
  5. Playbook 还评估了替代方案:把 TCP 监听换成 Unix domain socket(~/.claude-mem/worker.sock)。结论是 UDS 更安全但可能破坏 Windows 兼容性,因此最终选择 per-user 端口方案。

当前源码中的落地形态

当前代码中,按 UID 派生端口的逻辑直接体现在设置默认值层。SettingsDefaultsManager.ts(第 137 行):

CLAUDE_MEM_WORKER_PORT: String(37700 + ((process.getuid?.() ?? 77) % 100)),

从源码结构看,这一行与 Playbook 方案在思路上完全一致——默认端口不再是纯常量,而是由 UID 派生的每用户值process.getuid?.() 带 fallback 77,兼容没有 getuid 的平台如 Windows;模 100 把偏移收敛到 100 个端口的窗口内)。基座值在演进中从 37777 调整为 37700 区间,但「UID 派生 + 用户可覆盖」的契约保持不变:

  • 实际取端口值的是 worker-utils.ts 中的 getWorkerPort()(第 127–135 行),它从 settings.json 读取 CLAUDE_MEM_WORKER_PORT 并缓存——用户显式设置的端口(写入 settings 的)永远优先于派生默认值,满足 Playbook 第 3 条的「显式覆盖」约束;
  • 所有 hook、MCP server、CLI 都通过 buildWorkerUrl()(第 172–174 行)统一拼 http://host:port,因此端口派生只需要在默认值这一处生效,全链路自动跟随,这正是 Playbook 强调「单一派生点」的价值。

此外,CLAUDE_MEM_QUEUE_REDIS_PREFIX 默认值(第 227 行)同样内嵌了端口/UID 派生表达式,说明该隔离策略被推广到了队列命名空间,进一步避免多用户共享 Redis 时的前缀冲突。

四、GitHub Actions 注入疑虑的审计结论(#1285)

Playbook 对 #1285 的处理方式是审计后关闭而非改代码。其结论是:逐行审查所有工作流文件后,确认不存在可利用的注入向量,具体证据链如下——

  • convert-feature-requests.yml 使用 actions/github-script@v8,通过 GitHub API 调用完成任务,不经过 shell 插值;
  • claude.yml 使用 Anthropic 官方 claude-code-action(当前仓库 .github/workflows/claude.yml 第 35 行为 anthropics/claude-code-action@v1);
  • npm-publish.yml 走标准 npm 发布流程,无不可信输入参与;
  • deploy-install-scripts.yml 只使用硬编码路径;
  • summary.yml 引用 github.event.issue.number,这是 GitHub API 返回的可信数值型字段。

对照当前仓库的 .github/workflows/ 目录,工作流集合已扩展为 8 个文件(ci.ymlclaude.ymlclose-tracked-issues.ymlconvert-feature-requests.ymldeploy-install-scripts.ymlnpm-publish.ymlsummary.ymlwindows.yml),新增文件延续了同样的安全模式:

  • 全仓库范围内 grep run: 块,没有任何一处直接把 ${{ github.event.* }} 拼进 shell 命令;
  • summary.yml 的正确姿势是把事件值放进环境变量再交给脚本消费:
run: |
  ...
env 中:
  ISSUE_NUMBER: ${{ github.event.issue.number }}

github.event.issue.number 是数值类型,且经 env 通道传递(而非字符串插值进 shell),两条防线叠加后不构成注入面。

Playbook 给出的关闭评论模板也值得保留作为方法论参考:

Audited all 6 GitHub Actions workflows. No ${{ github.event.* }} values are interpolated into shell run: commands. convert-feature-requests.yml uses actions/github-script with API calls. All workflows follow secure patterns. Closing as not-a-vulnerability.

这套审计方法可以泛化:判断 CI 是否可被 issue/PR 标题描述注入,核心是检查 run: 步骤里的表达式插值,以及外部输入走的是 env、API 参数还是 shell 字符串拼接。

五、安全审计响应:已确认安全的模式与遗留考量(#1251)

#1251 是一次综合性审计请求,Playbook 要求产出一份安全审计响应文档,把「当前安全姿态」固化成可复查的记录。其中列出的已确认安全模式均可在当前源码中逐一核实:

1. Worker 只绑定 localhost,管理端点有 requireLocalhost 中间件

worker 服务通过 server.listen(port, host) 启动(worker-service.ts 第 420 行),host 默认来自 CLAUDE_MEM_WORKER_HOST 设置。管理端点由 middleware.tsrequireLocalhost()(第 64 行起)守卫,只放行 127.0.0.1::ffff:127.0.0.1localhost 三类回环来源,其余直接返回「Admin endpoints are only accessible from localhost」并以 SECURITY 分类记录拒绝日志:

export function requireLocalhost(req: Request, res: Response, next: NextFunction): void {
  ...
  clientIp === '127.0.0.1' ||
  clientIp === '::ffff:127.0.0.1' ||
  clientIp === 'localhost';

2. CORS 仅限 localhost 源

同一中间件文件第 47 行,CORS 校验只接受 http://localhost:http://127.0.0.1: 前缀的 Origin,防止本机其他浏览上下文跨源读取 worker API。

3. 路径边界校验与设置文件合并策略

上文第二节的 startsWith 白名单校验属于第 3 类。此外,设置加载采用 merge-with-defaults 模式(SettingsDefaultsManager.ts 把用户 settings.json 与内置默认值合并),意味着缺失或非法的设置项会落回安全默认值(如上文第 2 节的端口派生默认值),而不是让服务带着未定义行为启动。

4. 遗留考量:本地文件权限

Playbook 还列出了两项尚未强制、但应落实的加固项:

  • ~/.claude-mem/settings.json 的文件权限应设为 user-only(0600);
  • 数据库文件 ~/.claude-mem/claude-mem.db 应同为 user-only。

这两项属于操作系统层权限卫生,配合「worker 只监听回环 + 按 UID 派生端口」的网络层隔离,构成完整的本地数据边界。

作为更宏观的背景,仓库另有两份安全文档可作为延伸阅读:docs/security.md 说明 server beta 端默认启用 API-key 认证(密钥以 cmem_ 为前缀、仅存储 SHA-256 哈希)、CLAUDE_MEM_AUTH_MODE=local-dev 回环豁免仅在显式双开关下生效且不得暴露在公网;SECURITY.md 则是漏洞报告的正式入口。本阶段(#1251)关注的本地多用户边界与 server 端认证边界互为补充,共同覆盖 claude-mem 的两类部署形态。

六、验证方式与回归要求

Playbook 对该阶段设定的验收标准是回归性的,可直接复现:

  1. 运行 npm test —— 全部测试必须通过(其中安全修复配套新增 11 个路径校验测试与 9 个端口派生测试,仓库测试基线见 tests/ 目录下的 tests/worker/tests/shared/ 等子目录);
  2. 运行 npm run build-and-sync —— 构建产物与同步链路正常。

从仓库结构看,相关行为约束散落在多个测试簇中:worker 端口/健康检查相关契约见 tests/shared/worker-spawn-gate.test.tstests/services/worker-spawner.test.ts,中间件与端口绑定相关断言集中在 tests/infrastructure/tests/worker/http/ 目录。修改 writeAgentsMd()getWorkerPort()requireLocalhost() 等任一安全关键点时,都应把这两组测试作为回归门槛。

小结

TRIAGE-05 阶段的四条线勾勒出 claude-mem 本地端的安全模型:用户可控输入(watch 配置路径)必须在写入边界做白名单校验——当前实现收敛到「cwd + DATA_DIR 双根白名单 + .git 路径拦截 + 原子写」三层;本地回环服务不是天然安全的——多用户共享主机时固定端口会造成跨账户数据串扰,按 UID 派生端口并把显式覆盖权交给用户是低成本的隔离手段;CI 审计以「输入是否进 shell」为判定标准——env 通道传递可信数值字段、github-script 走 API 调用,构成无注入面的工作流范式;最后以 requireLocalhost、CORS 同源限制、merge-with-defaults 等已确认模式加上本地文件 0600 权限卫生,形成一份可复查的安全审计记录。这四类做法对任何「常驻本地服务 + 用户配置文件 + CI 自动化」形态的开发者工具都有直接参考价值。

登录后查看全文
热门项目推荐
相关项目推荐