dsh-plugin-safety
Install-time safety gate for the DSH plugin market: scans npm/GitHub plugins before install, passes safe ones silently, opens a review dialog for risky ones.
- Stars
- 0
- Language
- JavaScript
- Created
- Sep 8, 2026
- Updated
- Sep 8, 2026
Introduction
dsh-plugin-safety · DSH 插件安装安全检查
在 DeepSeek Harness 里安装第三方插件(插件市场 dsh-market / Community Market)时自动执行安全检查:
- ✅ 安全 → 直接放行,正常安装,无任何打扰;
- ⚠️ 有风险 → 弹出审查报告(评分 + 发现明细),由你拍板:仍然安装 / 取消 / 信任并加入白名单。
这不是一个“官方审核”结论,而是一套可解释的启发式检测:每个发现都带规则名、严重度、证据片段,白纸黑字告诉你为什么。
原理
市场 UI 点击「安装」
│ fetch POST /dsh-market/install {url}
▼
本插件客户端包裹 window.fetch ──► 调用宿主 POST /api/plugin-safety/check
│
┌─────────────────────────────────┴──────────────────────────────┐
│ npm 来源 GitHub 来源 │
│ · registry 版本元数据 · api.github.com 仓库元数据 │
│ · dist.integrity 校验 · codeload 仓库归档静态扫描 │
│ · tarball 下载 + 静态扫描 (与市场安装同一 HEAD 源码) │
└─────────────────────────────────────────────────────────────────┘
│ verdict = safe / caution / risky / unknown
┌────────────────────────────────────┴───────────────────────────┐
│ safe → 原样放行市场请求(正常安装) │
│ 其他 → 弹窗展示评分与发现 → 用户选择: │
│ 仍然安装(放行)/ 取消(返回市场已识别的 cancelled 响应)/ │
│ 信任并加入白名单(放行 + 记住) │
└─────────────────────────────────────────────────────────────────┘
- 绕过点:凡是经市场 UI 发起的安装(
/dsh-market/install,以及 Community Market 的 preview/execute 双步),都会被同一层 fetch 拦截覆盖。 - 检查在主机侧完成(Node.js 全局 fetch),对 npm 只访问
registry.npmjs.org/api.npmjs.org,对 GitHub 只访问api.github.com/codeload.github.com;与市场安装走同一信任根,tarball 做了dist.integrity字节级校验。 - 结果按目标缓存 6 小时;并发安装共享同一次扫描。
安装
方式一:手动(本机即时可用)
# 1. 把本插件加入 DSH 桌面 profile(和官方市场安装同样的方式)
cd ~/.dsh/profiles/desktop
pnpm add file:/绝对路径/dsh-plugin-safety
# 2. 把插件写入 bundle 列表(你的 profile package.json)
# 在 "dsh": { "profile": { "bundles": [...] } } 末尾追加 "dsh-plugin-safety"
然后重启 DSH Desktop(宿主插件 + 客户端模块都需重启加载)。
方式二:通过插件市场(发布后)
-
npm publish到 npm。本包publishConfig.registry已锁定https://registry.npmjs.org/,因此本机~/.npmrc里配的只读镜像(npmmirror)不会影响发布;prepublishOnly会先跑全部离线测试。 -
向 awesome-dsh-plugin 提交收录。按贡献指南,一次 PR 只加一个文件
data/plugins/RedMaple96__dsh-plugin-safety.yml(两个 README 由脚本生成,不要手改):url: https://github.com/RedMaple96/dsh-plugin-safety name: RedMaple96/dsh-plugin-safety category: security description: en: 'Scans npm / GitHub plugins before the DSH market installs them: safe ones pass silently, risky ones open a review dialog.' zh: 在市场安装三方插件前自动静态扫描,安全的直接放行,有风险的弹窗展示证据由你决定。硬性要求:仓库声明
dsh.bundle(本仓库已有)、仓库创建满 1 天(CI 自动检查)、仓库加dsh-plugintopic。市场优先用 npm 包安装,因此发布到 npm 后条目会自动带上 npm 来源,用户安装时无需本地构建。 -
收录后即可在市场里一键安装,之后本插件会在安装时自动检查它自己 —— 相当公平。
配置
插件设置页(设置 → 插件 → 安装安全)里可调:
| 项 | 默认 | 说明 |
|---|---|---|
| 启用检查 | 开 | 关闭后直接放行所有安装 |
| 需要确认的最低风险 | caution | caution=任何提示都要问;risky=只对高风险拦截 |
| 白名单 | 空 | 每行一个:npm 包名 / github owner/repo / 目标 URL,命中直接放行 |
| 黑名单 | 空 | 同上,命中直接返回“高风险”拦截 |
黑名单/白名单匹配大小写不敏感:路径形规则(owner/repo、URL)按子串命中(如 omdsh-dev/dsh-better-sidebar),裸包名按整段/整名匹配(规则 dsh 不会连带放行 dsh-plugin-safety);pkg 同时匹配 pkg@1.2.3。
判定规则(可解释)
包内容(静态扫描)——对 tarball/仓库归档里的 JS/TS 源码逐文件检测:
| 发现 | 分值 | 说明 |
|---|---|---|
| 远程代码执行链(下载+eval+子进程同现) | 12 | 高危组合 |
| 动态代码执行(eval/new Function/vm) | 12 | |
| 可疑生命周期脚本(curl/wget/bash/base64) | 12 | postinstall 里下载执行 |
| 连接硬编码公网 IP / 云元数据地址 | 12 / 14 | 数据外传信号 |
| 持久化机制(launchctl / reg add / /etc/systemd) | 10 | 自启动 |
| 访问密钥路径 + 文件 IO 同窗出现(~/.ssh、id_rsa、.aws/credentials) | 10 | 160 字符窗口内才计分 |
| 子进程执行 / 写用户主目录 | 8 | 各 |
| 监听网络端口 / 终端命令解释器调用 | 8 / 5 | |
| 生命周期脚本(install 系列 8 分;prepare 4 分) | — | GitHub 安装会真实执行 prepare |
| 大体积 Base64 载荷 / 解密例程 / 原生模块链 | 6 | 混淆信号 |
| 读取密钥类环境变量 / 明文 HTTP 外联 / WebSocket | 4–5 | 本地地址、w3.org、模板串已被排除 |
| fetch 外部 URL / 安装时编译链 / 原生构建声明(gypfile) | 3–5 | |
| 缺失完整性校验 / 单一维护者 | 1 |
元数据信号(npm / GitHub):刚发布 <7 天(+7)、下载量极低(+35)、无仓库/许可证(+23)、stars 极低(+2~5)、仓库已归档(+5)、仿冒包名(+8,Levenshtein ≤2 且剥离标点后不同)。
判定档位:risky ≥ 20 分;caution ≥ 8 分或存在 critical 发现或扫描不完整;其余 safe;目标完全无法解析 → unknown(也会弹窗)。
扫描覆盖:读不到的代码不会算安全。归档条目超上限、代码文件数超过分析上限、单文件超过体积上限、gzip 归档损坏或 tarball/归档请求返回 4xx/5xx,都会让结果升为 caution(或直接判 unknown 弹窗),而不是悄悄放过。npm 包中 dist/、build/ 是实际运行的产物,因此对 npm 来源会扫描(.min.、sourcemap 仍跳过);GitHub 仓库归档仍跳过这些构建产物,只扫源码。
误报纪律:测试目录、.test.ts、文档、本地地址、XML 命名空间、~/.ssh 出现在 UI 文案(无 IO 语境)等都不计分 —— 已知误报源处理如下:静默地址(127.x/私有网段)、w3.org、http://${…} 模板、*.test.ts/evals/ 目录、shell/ps1 脚本文件(仅查其被引用的生命周期脚本)。monorepo 里非安装目标的 workspace 包 manifest 只做情报展示(0 分),只有其中的可疑脚本内容(下载并执行)仍计分。
局限(请知悉)
- 启发式而非证明:零混淆代码可以执行任意行为而"看起来安全";反向也能误报。它提高门槛,不代替对作者的判断。
- npm 安装时生命周期脚本仅当市场允许执行才会运行;本插件对
install系列脚本一律计分,宁紧勿松。 - 只拦截经市场 UI 的安装;终端/手工
pnpm add不经过该流程(将来可在宿主侧挂 profile 变更钩子扩展)。市场的「更新」(/dsh-market/update)请求体只有包名、不含来源 URL,当前无法检查,故未纳入。 - 若 GitHub API 限流,元数据信号缺失(计 2 分并注明),源码扫描不受影响。
- Community Market 的 execute 请求只带
previewId:插件靠 preview 响应记住包名。若记忆丢失(插件刚重载、市场契约变更),会弹出"无法确认目标"的询问而不是直接放行;卸载(uninstall)不属于安装,直接放行。
安装目标识别
宿主收到的 target 可能是这些形态,全部会被解析到同一套扫描:
| 形态 | 示例 |
|---|---|
| npm 包名 | dshmarket |
| npm 带版本 | dshmarket@1.45.0、@scope/pkg@2.0.0 |
npm: 前缀 | npm:dshmarket@1.45.0、npm:@scope/pkg |
| GitHub 简写 | github:owner/repo、github:owner/repo#path:/packages/foo |
| GitHub URL | https://github.com/owner/repo、git@github.com:owner/repo.git |
monorepo 子路径安装(#path:/packages/foo)会优先采用该子包的 package.json 判定生命周期脚本 —— 真正被安装执行的是它。
宿主接口与来源校验
| 路由 | 方法 | 校验 |
|---|---|---|
/api/plugin-safety/check | POST | 同源 Origin + 非 sec-fetch-site: cross-site |
/api/plugin-safety/config | GET | 允许无 Origin(同源 GET 浏览器不带该头),拒绝跨站标记或 Origin 不符 |
/api/plugin-safety/config | POST | 同源 Origin + 非 sec-fetch-site: cross-site |
写操作必须带 Origin(浏览器对同源 POST 一定发送,缺失即为伪造/CSRF);读操作不能要求 Origin,否则 GUI 自己的配置读取会被 403 挡掉。
本次审查修复的问题
| 问题 | 影响 | 修复 |
|---|---|---|
GET /config 强制要求 Origin | 浏览器同源 GET 不带 Origin → 配置接口恒 403,设置卡片显示加载失败 | 读操作改为"仅拒绝可证明的跨站请求"(lib/guard.js) |
| GitHub 归档请求不检查 HTTP 状态 | 404/403 错误页被当 tar 解析 → 0 条目 → 判定 safe(严重漏报) | 校验状态码 + 空归档抛错 → 返回 unknown 弹窗 |
| tarball 请求不检查 HTTP 状态 | 同上(npm 侧被完整性校验兜住,但仍可能误判) | 校验状态码 |
| gzip 解压失败静默回退"裸 tar" | 损坏/超限归档变成空归档 → safe | 有 gzip 魔数却解压失败即抛错 |
| tar 字节上限截断无标记 | 只扫描了部分文件却报告完整 | 返回 truncated,并入 caution |
| 代码文件数上限无标记 | 超出 maxFiles 的文件静默不扫 | analyzeEntries 返回覆盖度,缺口 → caution |
npm 包跳过 dist/、build/ | 只发编译产物的包一行代码都没扫 | npm 来源扫描构建产物(.min./sourcemap 仍跳过) |
版本化目标 pkg@1.2.3 解析失败 | 社区市场每次安装都退化成"无法完成检查" | 解析 name@version(含 scope)并透传给 registry |
npm:@scope/pkg 解析为空 | 目标无法识别 | 修正解析 |
嵌套 package.json 找不到 | monorepo 子包/兄弟包的脚本永远看不见,相关代码为死代码 | 递归发现 manifest,子路径优先 + 兄弟包情报化 |
多个 workspace 包各计一次 prepare | 修好嵌套发现后会误判 monorepo 为 risky | 兄弟包 manifest 只报情报(0 分),可疑脚本内容仍计分 |
SSRI 多摘要 dist.integrity 解析失败 | 合法包被误报"校验和不符"(critical) | 逐 token 校验,支持 sha512-… sha1-… 与 ?options |
| 白名单子串匹配过宽 | 规则 dsh 会连带放行 dsh-plugin-safety 等无关包 | 裸名按整段匹配;路径形规则才做子串 |
| 弹窗已开时第二次安装直接放行 | 并发安装绕过安全检查 | 决定排队(decisionQueue),逐个询问 |
| 社区市场 execute 先删记忆再询问 | 取消后重试 = 未检查直接执行 | 取消时保留记忆,仅在真正执行后删除 |
| 「信任并加入白名单」用旧配置回写 | 覆盖掉其它新增白名单条目 | 写入前先重新拉取配置 |
设置卡片忽略注入的 t | 切换语言后卡片不更新;localeBind 兜底返回函数本体 | 卡片使用 props.t,并订阅 locale 变化 |
| 字典注册未返回 disposer | 插件重载后无法重新注册字典 | ctx.effect 返回 disposer |
| 设置 watcher 未释放 / 重复注册抛错 | 重载后监听泄漏、插件可能加载失败 | 释放 watcher + 注册失败降级为告警 |
verify-live.mjs 在桌面端必然失败 | 桌面宿主拒绝无 token 的请求(403 forbidden) | 支持 DSH_RENDERER_TOKEN,被守卫拦截时报告"未能验证"(exit 2) |
开发
node scripts/selftest.mjs # 离线引擎测试(tar / 规则 / 解析 / 覆盖度 / 白黑名单 / 路由守卫 / stub 端到端)
node scripts/selftest.mjs --live # + 真实扫描 dshmarket / dsh-better-sidebar
node scripts/verify-host.mjs # 宿主入口端到端:真实 apply() + stub 的 webServer/settings,校验 403/405/策略流
node scripts/verify-client.mjs # 客户端 bundle 注册、放行、拦截、排队、社区市场双步
node scripts/verify-live.mjs [port] # 重启后验证(被桌面守卫拦截时 exit 2)
无运行时依赖:tar 解析、规则引擎、网络边界全部用 Node 内置模块实现(安全工具自身越小越好)。端到端用例通过替换 globalThis.fetch 与 module.registerHooks(把 @deepseek-ai/schemastery 映射到 scripts/fixtures/)完成,不联网也能覆盖 npm/GitHub 两条解析链路与宿主路由。