首页
/ get-shit-done SDK 安全加固剖析:relPlanningPath 对工作流名的路径穿越校验(Issue 3589)

get-shit-done SDK 安全加固剖析:relPlanningPath 对工作流名的路径穿越校验(Issue 3589)

2026-09-04 22:06:49作者:卓炯娓

本文基于仓库中的变更集文档 .changeset/3589-planning-paths-workstream-validation.md,深入讲解 get-shit-done(GSD)SDK 中一次针对 planning 路径拼接的路径穿越(path traversal)安全修复。读完本文,你将理解 GSD 多工作流(workstream)机制下 .planning/ 路径是如何按工作流路由的、relPlanningPath 为何成为所有消费方共享的"失败关闭(fail closed)"校验缝合点,以及显式参数与环境变量来源的工作流名在安全语义上为何必须采用不同的处理策略。

1. 问题背景:多工作流机制与 planning 路径路由

GSD 支持在同一项目中按工作流(workstream)隔离规划产物。根据 workstream-utils.ts 头部注释,当提供 --ws <name> 参数时,所有 .planning/ 路径都会被路由到 .planning/workstreams/<name>/ 子树。这条路由逻辑的核心就是 relPlanningPath()

export function relPlanningPath(workstream?: string): string {
  if (!workstream) return '.planning';
  if (!validateWorkstreamName(workstream)) {
    throw new Error(
      `Invalid workstream name: ${JSON.stringify(workstream)}. ` +
        `Workstream names must match /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/ and may not contain '..'.`,
    );
  }
  // Use POSIX segments so the same logical path string is used on all platforms (Windows included).
  return posix.join('.planning', 'workstreams', workstream);
}

(源码见 workstream-utils.ts

围绕它有两类主要消费方:

  1. planningPaths(projectDir, workstream?):返回一套完整的规划文件路径对象(planningstateroadmapprojectconfigphasesrequirements),内部通过 join(projectDir, relPlanningPath(...)) 构造基路径(见 query/helpers.ts)。
  2. ContextEngine:SDK 的上下文引擎在构造器中直接执行 this.planningDir = join(projectDir, relPlanningPath(workstream))(见 context-engine.ts),后续所有上下文文件的解析都基于这个 planningDir

此外,SDK 入口 index.ts 对外同时导出了 validateWorkstreamNamerelPlanningPath,CLI 侧 cli.ts--ws 缺省时也会回退读取 GSD_WORKSTREAM 环境变量并用同一策略做校验。也就是说,工作流名到文件路径的映射散落在多个入口,而这些入口最终都汇聚到 relPlanningPath 这一条路径构造缝上。

2. 漏洞本质:显式参数缺失路径穿越门禁

变更前,relPlanningPath 对显式传入的 workstream 参数没有任何校验。这意味着一个直接调用 SDK 的一方可以传入诸如 '../../../outside' 这样的值,它会原样流经 posix.join('.planning', 'workstreams', name),把本应落在 .planning/workstreams/<name> 子树内的所有规划操作——读取 STATE.mdROADMAP.md 等——悄悄路由到项目根之外的任意目录。

需要注意漏洞面与正常调用路径之间的差异:

  • CLI 与 gsd-tools 路径--ws 参数或 GSD_WORKSTREAM 环境变量在 cli.ts 中已经过 validateWorkstreamName 过滤,非法值被丢弃而非直接用于拼路径。
  • planningPaths 内部的环境变量来源resolveWorkspaceContext()(见 query/workspace.ts)读到的 GSD_WORKSTREAM 会在 planningPaths 中被 validateWorkstreamName 预校验,非法值被置为 null 并静默回退到根 .planning/——这是 issue #2791 确立的契约。
  • 直接 SDK 调用方relPlanningPathplanningPaths(projectDir, workstream) 的第二个参数、以及 ContextEngineworkstream 构造参数,在修复前都没有任何门禁。这正是 #3589 关闭的缺口。

3. 修复方案:在共享缝合点统一执行校验策略

修复思路不是在每个调用点各加一遍校验,而是把共享的 validateWorkstreamName 策略直接下沉到 relPlanningPath 内部,使所有消费方在同一缝合点失败关闭(changeset 原文表述为 "every consumer fails closed at the same seam")。校验时机刻意放在路径构造之前,非法名字会同步抛出异常,而不是构造出一个半成品的越界路径。

校验策略本体位于 workstream-name-policy.ts,这是 CJS 运行时(active-workstream-store.cjsplanning-workspace.cjsworkstream.cjs)与 SDK 层共享的规范策略模块,其中:

  • ACTIVE_WORKSTREAM_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/第 12 行):名字必须以字母或数字开头,仅允许字母、数字、点、下划线、连字符;
  • isValidActiveWorkstreamName第 52-56 行)额外显式拒绝任何包含 .. 的序列,堵住路径穿越;
  • hasInvalidPathSegment第 41-44 行)提供另一维度的判定:含路径分隔符、裸 ./..、或出现 .. 子序列的名字均不安全;
  • validateWorkstreamNameisValidActiveWorkstreamName 的 SDK 层别名,toWorkstreamSlug 则负责把展示名规范化为文件系统安全的 slug。

由此可以枚举修复前后的行为矩阵:

输入 修复前 修复后
undefined / '' 返回 .planning 返回 .planning(空字符串按"未提供"处理,保持向后兼容)
合法名(如 frontendapi_v2alpha.beta-1 返回 .planning/workstreams/<name> 不变
'../../../outside''../escape''.. ''foo/bar''foo\\bar''foo bar''.hidden''/abs''-leading-hyphen' 静默构造越界路径 同步抛出 Invalid workstream name: ...

4. 双轨语义:显式参数 fail-closed,环境变量静默回退

修复中最值得推敲的设计是:同一条代码路径上,对"显式参数"和"环境变量来源"采用了截然相反的失败语义

planningPaths 中:

const envCtx = resolveWorkspaceContext();
// Validate env workstream before use: invalid GSD_WORKSTREAM falls back to
// root .planning/ (bug-2791 contract — invalid env must not crash or route
// to a bad path; silent fallback to root preserves pre-#3269 behaviour).
const validEnvWorkstream =
  envCtx.workstream && validateWorkstreamName(envCtx.workstream) ? envCtx.workstream : null;
const effectiveWorkstream = workstream ?? validEnvWorkstream;
const base = join(projectDir, relPlanningPath(effectiveWorkstream ?? undefined));

两条语义可以这样理解:

  1. 显式参数(fail-closed):调用方在代码里主动写死了工作流名,这个名字错了更可能是调用方 bug 或恶意输入,静默回退会让问题被掩盖,因此必须在 relPlanningPath 内抛错让调用方立刻感知。
  2. 环境变量(silent fallback,#2791 契约)GSD_WORKSTREAM 是外部注入的环境状态,一个陈旧或非法的 env 值不应该让工作流"崩溃或路由到坏路径";planningPaths 在进入 relPlanningPath 之前就把非法 env 值过滤为 null,从而静默回退到根 .planning/,保持了 #3269 之前的行为。也就是说,env 值根本不会到达 relPlanningPath 的抛错分支,新的守卫不会改变 env 来源的任何既有行为——这正是 changeset 文档强调的最后一段内容。

这种"校验下沉 + 前置过滤"的组合,让单一入口同时满足两种失败语义,且两者的优先级清晰:显式参数优先于 env(workstream ?? validEnvWorkstream)。

5. 测试证据:穿越用例与"先抛错再构造"保证

回归测试 bug-3589-planning-paths-validation.test.ts 完整固化了上述契约:

  • TRAVERSAL_CASES 列出 9 个非法名字('../../../outside''../escape''..''foo/bar''foo\\bar''foo bar''.hidden''/abs''-leading-hyphen'),逐个断言 relPlanningPath 抛出匹配 /workstream/i 的异常(第 26-55 行);
  • 显式验证了"在构造路径之前就抛出":用 try/catch 包裹 relPlanningPath('../../../outside'),断言不存在任何部分路径产出(第 57-65 行);
  • planningPaths 层面的对称测试:显式非法名抛错,合法名构造出 …/.planning/workstreams/frontend/{STATE.md,ROADMAP.md} 等预期子树,省略参数时回根 .planning第 68-88 行);
  • 兼容性测试确认 relPlanningPath()relPlanningPath(undefined)relPlanningPath('') 三者都返回 .planning,保证修复对既有调用方零破坏。

6. 对 SDK 使用者的实践启示

从源码结构看,这条修复确立了 GSD SDK 中工作流名处理的三个可复用原则:

  1. 路径构造与校验必须同缝:任何把外部标识符拼进文件路径的函数,都应在拼路径前执行策略校验并同步抛错,而不是指望上游各调用方自觉。relPlanningPath 现在是唯一的路径构造缝,planningPathsContextEngine 都自动受益。
  2. 策略集中、多处共享:命名策略收敛在 workstream-name-policy.ts 一个模块,CJS 运行时与 SDK 层共同消费,避免校验规则漂移;仓库内 shared-module-handsync-allowlist.json 等脚本也在守护这类共享模块的一致性。
  3. 区分输入来源的失败语义:代码内显式参数应 fail-closed,环境注入值可按契约静默回退——但前提是回退分支必须在到达共享校验缝之前完成过滤,两者不可混用。

如果你在自己的代码中集成 GSD SDK 并需要自定义工作流路由,正确的做法是先调用公开导出的 validateWorkstreamName 预判合法性,或直接信任 relPlanningPath / planningPaths 的同步抛错来捕获非法名,而不是自行用 path.join 拼接 .planning/workstreams/<name>

小结

#3589 是一个典型"共享工具函数缺少输入门禁"的安全修复:它把校验从"分散在各入口"收敛到 relPlanningPath 这唯一的路径构造缝合点,使直接 SDK 调用、planningPathsContextEngine'../../../outside' 这类穿越输入一律失败关闭;同时通过 planningPaths 中对 GSD_WORKSTREAM 的前置过滤,完整保留了 #2791 确立的"非法环境变量静默回退根 .planning/"契约。变更集文档 .changeset/3589-planning-paths-workstream-validation.md 将其分类为 Security 类型,配套回归测试位于 sdk/src/bug-3589-planning-paths-validation.test.ts

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341