【免费下载链接】gsd-core
Git. Ship. Done - Core
本指南围绕 gsd-core 的 Security Hardening 特性展开,系统讲解这套以"纵深防御(defense-in-depth)"为原则的安全体系:它为何存在、由哪些组件构成、每个组件在源码中如何实现、如何配置与验证。读完你将掌握 GSD 项目中防止路径穿越、检测提示注入、防御 Shell 与 JSON 注入的完整技术方案,并能独立运行 CI 注入扫描器检验自己的变更。
为什么 GSD 需要安全加固
gsd-core(Git. Ship. Done)是一款面向 LLM 的规划与执行工作流工具:它生成大量 Markdown 文件(agent 指令、workflow 状态、phase 计划),而这些文件最终会成为 LLM 的系统提示词(system prompts)。这意味着一个关键的安全推论:
任何用户可控文本一旦流入这些文件,都可能成为**间接提示注入(indirect prompt injection)**的载体——攻击者把看似普通的文本(如 PRD、issue、任务描述)嵌入其中,让模型误以为这是系统指令并执行恶意操作。
这一威胁模型在 src/security.cts 的模块头注释中被明确列出,共五类:
| # | 威胁 | 说明 |
|---|---|---|
| 1 | 路径穿越(Path traversal) | 用户提供的文件路径逃逸出项目目录 |
| 2 | 提示注入(Prompt injection) | 参数/PRD 中的恶意文本内嵌 LLM 指令 |
| 3 | Shell 元字符注入(Shell metacharacter injection) | 用户文本被 Shell 解释执行 |
| 4 | JSON 注入(JSON injection) | 畸形 JSON 导致崩溃或状态损坏 |
| 5 | 正则 DoS(Regex DoS) | 精心构造的输入引发灾难性回溯 |
针对上述模型,该特性拆分为四个组件:集中式安全模块、提示注入守卫 Hook、工作流守卫 Hook,以及 CI 就绪的注入扫描器。下面逐一展开。
组件一:集中式安全模块(security.cts)
这是整套体系的"中央引擎"。注意一个历史沿革:原文档中它被称为security.cjs,但根据 ADR-457(build-at-publish),手写的bin/lib/security.cjs已折叠为 TypeScript 单一事实源 src/security.cts,构建时生成gsd-core/bin/lib/security.cjs,行为逐字节保持一致。本文以下分析均针对src/security.cts。
1.1 路径穿越防护:唯一归属判定 + 双重解析策略
路径安全的核心是**"已解析路径必须落在项目目录内"**这一条铁律,由 isContainedIn 实现,它被注释为"本仓库判定路径归属的唯一规范位置"(ADR-4650):
- 对分隔符敏感:比较两侧都先补上尾部分隔符再做前缀匹配,避免
<root>-evil这种仅共享前缀的兄弟目录被误判为包含关系; target === root视为包含;- 支持注入
path.win32/path.posix,使 Windows 分隔符语义可在非 Windows 平台测试。
在其之上,validatePath 完成完整的输入校验流程,处理了五类细节:
- 空值与类型检查:非字符串、空串直接拒绝;
- NUL 字节拒绝:含
\0的路径立即失败; - 绝对路径策略:默认拒绝绝对路径,仅当显式传入
allowAbsolute时才放行(且仍需通过包含性校验); - 符号链接双保险:先
fs.realpathSync解析;若失败则用lstatSync判别"悬空符号链接"(链接存在但目标不存在)并拒绝——这一区分防止悬空链接指向外部不存在路径时被回退逻辑重新放行,也堵死了"存在性预言机"(existence oracle)侧信道; - 祖先回溯规范化:若目标本身尚不存在,则向上逐层找到最近存在的祖先并 realpath 之,再拼接剩余段。这正是 REQ-SEC-05 的实现基础——macOS 上
/var是/private/var的符号链接,若不做规范化,/var/...与/private/var/...的字符串前缀比较会误判(详见源码 L107-L132 的注释)。
对外暴露的四组 API 分工明确:
- 抛异常版:assertWithinRoot(CLI 命令使用)、requireSafePath(委托前者);
- 返回 null 版:tryWithinRoot——不安全时严格返回
null而非逃逸路径,因为旧版validatePath在穿越分支仍会填充resolved字段,直接返回它会复现"看似可用实则危险"的缺陷; - 类型品牌(branded type):ContainedPath 用
unique symbol标记,普通string不可赋值给它,持有ContainedPath即证明包含性检查已通过; - 纯词法版:tryWithinRootLexical / assertWithinRootLexical——只做
path.resolve不做任何文件系统访问,适用于"目标尚不存在"(如 mkdir 前校验目的地)或必须保留符号链接的场景,但源码明确警告:词法检查"看不见符号链接",依赖它做写约束的调用方必须自带符号链接拒绝逻辑。
此外,PathAcceptance 用命名策略取代了旧的{ allowAbsolute: true }布尔标志:RelativeOnly只接受相对路径;AbsoluteInsideRoot表示"绝对路径可参与判定,但包含性绝不放松——逃出根目录的绝对路径与穿越一样被拒绝"。
与路径安全配套的是可信全局根白名单加载器 loadTrustedGlobalRoots,读取配置config.agent_skills_security.trusted_global_roots,规则包括:展开~、拒绝非绝对路径(项目相对路径视为越界)、对每个条目做 realpath 规范化并去重,且拒绝文件系统根与 home 目录本身作为信任根(防止 macOS APFS 大小写不敏感的/users/alicevs/Users/alice绕过)。
1.2 提示注入检测:多层级模式扫描
scanForInjection 是代码库级扫描引擎,返回{ clean, findings, structuredFindings }结构化结果。它综合了三类检测源:
- INJECTION_PATTERNS:核心正则集,按攻击手法分组——指令覆盖(
ignore all previous instructions等)、角色操控(you are now a...、act as a...,其中act as显式排除了plan|phase|wave以减少误报)、系统提示词提取(print/reveal your system prompt)、伪造边界(</system>、[SYSTEM]、<<SYS>>)、外泄(curl/wget https://、base64 ... send)、工具操控(run bash tool)。注释特别说明:<instructions>被有意排除,因为它是 GSD 的合法提示结构; - MARKDOWN_LINK_PATTERNS:Markdown 链接攻击面,含
javascript:协议、data:URI(仅放行白名单 MIME:image/png|jpe?g|gif|webp...与font/*,SVG 被有意排除因其可内嵌<script>)、含用户信息的 URL、查询串中的令牌泄露(token、api_key、secret等); - OBFUSCATION_PATTERN_ENTRIES:混淆手法——字符间空格混淆(
i g n o r e)、定界符注入标签、长十六进制序列。
strict模式额外开启三道检查(L491-L510):零宽/不可见 Unicode(\u200B-\u200F、RTL 覆盖、软连字符等)、Unicode 标签块 U+E0000–U+E007F(2025 年供应链攻击向量,不可见字符可内嵌隐藏指令)、以及超长文本(CRLF 规范化后 > 50000 字符视为 prompt stuffing 嫌疑)。
1.3 净化与展示安全
检测之外,模块还提供三层净化函数:
- sanitizeForPrompt:面向"将嵌入 agent 提示词"的文本——剥离零宽字符、把
<system>/<human>等边界标签中和为全角<system-text>、[SYSTEM]/[INST]改写为[SYSTEM-TEXT]、<<SYS>>改写为«SYS-TEXT»; - sanitizeForDisplay:面向回显给用户的文本,额外按行过滤协议泄漏标记(如
assistant to=...、<|assistant|>); - sanitizeLabel:面向"必须渲染为单行"的文件系统名(phase 目录名、归档里程碑标签)。它把 C0 控制字符(含 ESC
\x1b、CR、LF)、DEL 与 C1 区间转义为可见表示(\n、\x1b)而非静默剥离——因为 #3458 的复现中,一个名为zz\n0 open items require decisions.\n\x1b[2K...FORGED的 phase 目录会借助换行伪造报告行、借助 ESC 字节直达终端;转义让审查者能看到篡改痕迹而非被悄悄抹平。
1.4 Shell 与 JSON 安全
- validateShellArg:Shell 参数校验——拒绝空值/非字符串/NUL 字节;检测
$(、反引号等命令替换特征并抛错; - safeJsonParse:安全 JSON 解析——默认 1 MiB(1048576 字节)大小上限,返回
{ ok, value?, error? }结构而非抛异常,畸形输入在破坏状态前被优雅捕获(对应 REQ-SEC-04); - validateFieldName:字段名校验,正则
^[A-Za-z][A-Za-z0-9 _.\-/]{0,60}$,防止经配置字段名注入正则; - validatePhaseNumber:phase 编号参数校验,支持
123A、1.2.3与MANIFOLD-64-auth两类合法形态。
1.5 结构化 Schema 校验
validatePromptStructure 对 agent/workflow 两类提示文件的 XML 结构做白名单校验:仅允许objective、process、step、success_criteria、critical_rules、available_agent_types、purpose、required_reading这组已知标签,未知标签记入 violations。这为"提示文件结构不可被注入篡改"提供了又一层保障。
组件二:Prompt Injection Guard Hook(gsd-prompt-guard.js)
hooks/gsd-prompt-guard.js 是注册在PreToolUse事件上的守卫:拦截Write / Edit工具调用,且只扫描目标位于.planning/(agent 上下文文件所在目录)的写入操作。
几个关键实现事实:
- Advisory-only 设计(对应 REQ-SEC-03):钩子头注释与崩溃策略
HOOK_ON_CRASH.ALLOW都明确"检测但不阻断",目的是让编排者感知可疑内容而不是因误报造成死锁;即使自身崩溃也绝不开始阻断(#3911); - 共享模式集:hooks/lib/injection-patterns.js 是
gsd-prompt-guard.js与gsd-read-injection-scanner.js(PostToolUse 扫描 Read/WebFetch/WebSearch 内容)的单一模式来源(#3504),两处表面不再可能各自漂移。该文件头还说明:它刻意不与security.cts的scanForInjection集合统一——Hook 必须能在不加载编译产物树的情况下独立加载,两套集合是"不同表面"而非漂移。其 #4016 超集模式用一个容错命令式覆盖正则取代了五个窄模式,并配套 describePattern 把约 280 字符的正则源码压缩为 ≤50 字符的可读标签用于告警文案; - Kimi 负载归一化:normalizeKimiPayload 处理 kimi-cli 的工具词表差异(
WriteFile → Write、StrReplaceFile → Edit),并修复了字段遮蔽漏洞(#2547/#2595):path权威覆盖file_path、无条件重建old_string/new_string、对edit数组做带守卫的 String 强制转换——这些修复堵住了"模型伪造空字段使守卫读到空串而放行"的绕过路径; - 告警输出:命中时输出结构化
hookSpecificOutput,含additionalContext与findings数组(规则 IDINJECTION-PATTERN/INVISIBLE-UNICODE),提醒内容将进入 agent 上下文,请审查是否有内嵌指令;若内容是合法的注入讨论文档则正常放行。
在 hooks/hooks.json 中,它注册于PreToolUse的Write|Editmatcher 下,超时 120 秒。
组件三:Workflow Guard Hook(gsd-workflow-guard.js)
hooks/gsd-workflow-guard.js 解决另一类问题:模型绕过 GSD 工作流直接编辑文件,导致变更不被 STATE.md 跟踪、不产生 SUMMARY.md。
- 启用方式:通过项目配置
hooks.workflow_guard开启(默认 false)。读取位置是项目根下.planning/config.json中的config.hooks?.workflow_guard(见 workflowGuardEnabled); - 触发面:仅拦截 Write / Edit / MultiEdit 且目标不在
.planning/的调用;.planning/文件(GSD 状态管理)直接放行;.gitignore、.env、CLAUDE.md、AGENTS.md、GEMINI.md、settings.json等常规配置/文档也放行(allowedPatterns); - 子代理豁免:
is_subagent或session_type === 'task'时放行(编排场景); - 告警语义:触发时注入
WORKFLOW_ADVISORY建议文案,引导使用/gsd:fast(琐碎修复)或/gsd:quick(较大改动)以维持状态跟踪;用户显式要求直接编辑时可忽略; - 唯一的硬阻断:在 agent 分支(
/^(worktree-)?agent-/)上执行git add -f/--force被硬性拒绝(WORKTREE_AGENT_FORCE_ADD_FORBIDDEN),且该阻断在内部出错时失败关闭(fail-closed)(#3504):通过 failClosedBlockContext 从负载重新推导阻断上下文,无法确证 agent 分支时才退化为放行。advisory 分支则始终失败开放——一个坏掉的建议绝不能卡死每一次工具调用。
组件四:CI 就绪的注入扫描器
这一组件分两条线:Node 测试套件与 Shell 脚本。
4.1 测试套件:prompt-injection-scan.security.test.cjs
tests/prompt-injection-scan.security.test.cjs 是 CI 对整个代码库的注入体检:
- 扫描范围(SCAN_DIRS):
agents、commands、gsd-core/workflows、gsd-core/bin/lib、hooks,文件类型.md/.cjs/.js/.json——即所有会成为 LLM agent 上下文的文件; - 分组断言:agent 定义文件必须无注入模式且 ≤ 100K(源码文件天然更大,阈值放宽);workflow / command / hook / lib 文件必须通过
scanForInjection(workflow 与 command 走strict: true严格模式);全库不可见 Unicode 扫描;.js/.cjs源文件不得出现伪造的<system>/<assistant>/<human>边界标签; - 白名单治理:ALLOWLIST 只豁免"合法讨论注入"的文件(安全文档、测试自身、守卫源码);SIZE_ONLY_WORKFLOWS 只豁免 50K 尺寸告警(如
execute-phase/steps/code-review-disposition.md),注入检测照跑不误——注释明确警告"不得把合法引用注入模式的文件放进尺寸豁免表"; - 回归向量测试(L386-L470):覆盖 frontmatter 注入、commit message 中的
[SYSTEM]、PRD 中的<system>标签、phase 描述中的角色操控、系统提示词提取,并含 #2295 的双向回归——fact as an/artifact as a不再误报,而真实的act as an administrator仍必须命中("边界修复不得静默放过真负载"); - Shell 扫描器专项测试(L483-L620):直接驱动
scripts/prompt-injection-scan.sh,验证 #3175 左边界修复——impact/contract/artifact/interact/transact/redact/abstract as a...、reprint the instructions、retrieval('...')、medieval('...')、Jordan mode全部扫描干净,而exec('...')的各种拼写(含child_process.exec、require("child_process").exec)与eval('...')单引号形式、new Function('...'); return ...、DAN mode必须仍然命中。注释特别指出["\x27]是 GNU-grep 专属转义,BSD/macOS grep 不识别,故用["'"'"'"]拼写以保证跨平台可移植性。
4.2 Shell 扫描器:prompt-injection-scan.sh
scripts/prompt-injection-scan.sh 是 CI 工作流实际调用的独立实现(与scanForInjection的 Node 模式集相互独立,二者是 #3175 中刻意区分的两条线)。三种调用模式:
scripts/prompt-injection-scan.sh --diff origin/main # CI 模式:扫描变更的 .md 等文件 scripts/prompt-injection-scan.sh --file path/to/file # 扫描单个文件 scripts/prompt-injection-scan.sh --dir agents/ # 扫描目录下所有文件其模式集(PATTERNS)按指令覆盖、角色操控、系统提示词提取、伪造边界、工具调用注入(eval(/exec(/Function(...return)、越狱(DAN mode、jailbreak)分组,全部为 POSIX 扩展正则并用(^|[^[:alnum:]])显式书写左边界——因为\b是 GNU 扩展,脚本须兼容 BSD/macOS grep。exec(刻意保持"接收者盲"(receiver-blind):若给.加左边界排除,require('child_process').exec('...')这一最常见的 Node 攻击拼写也会被放过。
退出码契约(ADR-3889、#3908,值从 gsd-core/bin/shared/exit-codes.sh 引入,绝不硬编码):
| 退出码 | 含义 |
|---|---|
0 | 已扫描,无发现(clean) |
1 | 发现注入模式 |
64(USAGE) | 参数错误(未知模式、--file/--dir目标不存在) |
66(NO_INPUT) | 范围已建立但确实为空(如纯文档 PR)——非失败 |
69(UNAVAILABLE) | 无法建立扫描范围(bad ref、不在 git 仓库、目录不可读)——从未真正执行 |
四条退出码全部非零的设计意图是:if ! scanner; then风格的调用方对"干净扫描"与"扫描失败"行为一致——"假绿"只会变红,绝不会把"红"变绿。CI 中66视为通过、69视为失败,杜绝"扫了个寂寞还报绿"。
需求到实现的映射
原文档定义的五个安全需求均已落地,映射关系如下:
| 需求 | 实现位置 |
|---|---|
| REQ-SEC-01:用户路径必须针对项目目录校验 | validatePath / assertWithinRoot 的 realpath + 祖先回溯 +isContainedIn归属判定 |
| REQ-SEC-02:注入模式必须在进入规划产物前被检测 | gsd-prompt-guard.js 的 PreToolUse 扫描 + scanForInjection |
| REQ-SEC-03:安全 Hook 必须仅咨询(advisory-only) | 两个 Hook 的HOOK_ON_CRASH.ALLOW与"检测不阻断"策略 |
| REQ-SEC-04:JSON 解析必须优雅捕获畸形数据 | safeJsonParse 的{ ok, value?, error? }结构 |
REQ-SEC-05:macOS/var→/private/var符号链接必须处理 | validatePath 的最近祖先 realpath 回溯逻辑 |
配置与部署速览
- Hook 注册:
hooks/hooks.json已预置注册——gsd-prompt-guard.js挂在PreToolUse的Write|Edit(120s 超时),gsd-read-injection-scanner.js挂在PostToolUse的Read|WebFetch|WebSearch,gsd-worktree-path-guard.js挂在Write|Edit|MultiEdit。安装时这些脚本按GSD_HOOK_LIB_FILES白名单随hooks/lib/一起 staging(gsd-prompt-guard.js头注释与 hooks/lib/injection-patterns.js 有说明); - 开启工作流守卫:在项目
.planning/config.json中写入{ "hooks": { "workflow_guard": true } }(默认关闭); - 信任根白名单(可选):在配置的
agent_skills_security.trusted_global_roots数组中列出绝对路径,仅当确有跨目录读取需求时使用; - 本地自检:提交前运行
scripts/prompt-injection-scan.sh --diff origin/main,并执行node --test tests/prompt-injection-scan.security.test.cjs验证全库健康; - 延伸阅读:该特性的姊妹文档 docs/features/improved-prompt-injection-scanner.md 记录了后续演进(invisible Unicode 检测、退出码四态契约、
scanEntropyAnomalies熵分析因零调用方被移除的 #2198 决策);完整的安全态势说明见 SECURITY.md 与 docs/security/baseline.md。
设计要点回顾
- 纵深防御而非单一防线:集中式模块负责"输入验证引擎",Hook 负责"运行期拦截",CI 扫描器负责"变更门禁",净化函数负责"输出收口"——四层互相独立、各有侧重;
- Advisory-only 是刻意选择:阻断合法操作会制造误报死锁,检测并上抛上下文让编排者决策,是 LLM 工作流安全 Hook 的务实姿态(唯一的硬阻断
git add -f例外,且失败关闭); - 单一事实源与防漂移:
isContainedIn是全仓库唯一的归属判定;hooks/lib/injection-patterns.js是双 Hook 共享的模式来源;退出码来自注册表而非脚本字面量——三者共同防止"修了一处、漏了另一处"的漂移类缺陷。
【免费下载链接】gsd-core
Git. Ship. Done - Core
相关推荐
MCP 安全加固实战:用 Azure Content Safety 与 Prompt Shields 防御提示注入
MCP 安全加固实战:用 Azure Content Safety 与 Prompt Shields 防御提示注入 本文以 mcp for beginners
教程文档人工智能Places365应用场景:从智能监控到自动驾驶的10大实际用例
Places365应用场景:从智能监控到自动驾驶的10大实际用例 Places365作为强大的场景分类工具,通过深度卷积神经网络(CNNs)实现对365个场景类
人工智能计算机视觉深度学习预训练Spaceship Prompt 路径注入漏洞修复全解:spaceship::extract 的安全加固与防御实践
Spaceship Prompt 路径注入漏洞修复全解:spaceship::extract 的安全加固与防御实践 本文详细解析 Spaceship Promp
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考