claude-mem 安全加固实战:watch.context.path 路径穿越防护、多用户端口隔离与 CI 注入审计
本文围绕 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()。
问题链条很清晰:
- 配置加载函数 loadTranscriptWatchConfig() 读取并解析 JSON 配置,原实现中
context.path字段不做任何路径合法性校验; - 路径展开函数 expandHomePath()(
src/services/transcripts/config.ts第 56–62 行)只处理一个职责——把~前缀替换为用户主目录,对../之类的穿越序列不做防御; - 于是 processor 拿到
watch.context.path后直接用于写入,攻击者(或误配置)可以把上下文内容写到系统上任意可写位置。
Playbook 中的修复设计
Playbook 给出的修复要点是「在写入前做边界校验,同时保持合理路径可用」:
const resolvedPath = path.resolve(agentsPath)—— 先解析为绝对路径;const homeDir = os.homedir()—— 取用户主目录作为安全边界;- 若解析后的路径不以
homeDir开头,记录警告并提前返回; - 若解析后的路径仍含
..段,拒绝(纵深防御); - 同时在配置加载时(
loadTranscriptWatchConfig())提前拒绝任何解析到主目录之外的context.path; - 明确约束:校验不能过严,
~/.codex/AGENTS.md、~/project/AGENTS.md这类合法路径必须保持可用。
当前源码中的落地形态
对照当前仓库代码,路径穿越防护实际落在两层:
第一层:processor 侧的根白名单校验。 在 processor.ts 的 updateContext()(第 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.ts 的 writeAgentsMd()(第 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.ts 的 isNativeHookBackedCodexWatch()(第 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 派生端口,核心规则:
- 获取当前用户 UID:
process.getuid()(Unix)或os.userInfo().uid(跨平台); - 计算端口:
37777 + (uid % 1000),让每个用户落在 1000 个端口的区间内互不冲突; - 仅当用户没有显式设置
CLAUDE_MEM_WORKER_PORT时才应用派生逻辑——环境变量优先级始终最高; - 文档注释中的
37777需要同步更新,但设置默认值本身保持'37777'作为基座,派生逻辑单独一层; - 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.yml、claude.yml、close-tracked-issues.yml、convert-feature-requests.yml、deploy-install-scripts.yml、npm-publish.yml、summary.yml、windows.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.ts 的 requireLocalhost()(第 64 行起)守卫,只放行 127.0.0.1、::ffff:127.0.0.1、localhost 三类回环来源,其余直接返回「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 对该阶段设定的验收标准是回归性的,可直接复现:
- 运行
npm test—— 全部测试必须通过(其中安全修复配套新增 11 个路径校验测试与 9 个端口派生测试,仓库测试基线见 tests/ 目录下的tests/worker/、tests/shared/等子目录); - 运行
npm run build-and-sync—— 构建产物与同步链路正常。
从仓库结构看,相关行为约束散落在多个测试簇中:worker 端口/健康检查相关契约见 tests/shared/worker-spawn-gate.test.ts 与 tests/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 自动化」形态的开发者工具都有直接参考价值。
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 StartedRust0624
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