Back to home@RedMaple96

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
GitHub repo

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(宿主插件 + 客户端模块都需重启加载)。

方式二:通过插件市场(发布后)

  1. npm publish 到 npm。本包 publishConfig.registry 已锁定 https://registry.npmjs.org/,因此本机 ~/.npmrc 里配的只读镜像(npmmirror)不会影响发布;prepublishOnly 会先跑全部离线测试。

  2. 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-plugin topic。市场优先用 npm 包安装,因此发布到 npm 后条目会自动带上 npm 来源,用户安装时无需本地构建。

  3. 收录后即可在市场里一键安装,之后本插件会在安装时自动检查它自己 —— 相当公平。

配置

插件设置页(设置 → 插件 → 安装安全)里可调:

默认说明
启用检查关闭后直接放行所有安装
需要确认的最低风险cautioncaution=任何提示都要问;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)12postinstall 里下载执行
连接硬编码公网 IP / 云元数据地址12 / 14数据外传信号
持久化机制(launchctl / reg add / /etc/systemd)10自启动
访问密钥路径 + 文件 IO 同窗出现(~/.ssh、id_rsa、.aws/credentials)10160 字符窗口内才计分
子进程执行 / 写用户主目录8
监听网络端口 / 终端命令解释器调用8 / 5
生命周期脚本(install 系列 8 分;prepare 4 分)GitHub 安装会真实执行 prepare
大体积 Base64 载荷 / 解密例程 / 原生模块链6混淆信号
读取密钥类环境变量 / 明文 HTTP 外联 / WebSocket4–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.orghttp://${…} 模板、*.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.0npm:@scope/pkg
GitHub 简写github:owner/repogithub:owner/repo#path:/packages/foo
GitHub URLhttps://github.com/owner/repogit@github.com:owner/repo.git

monorepo 子路径安装(#path:/packages/foo)会优先采用该子包的 package.json 判定生命周期脚本 —— 真正被安装执行的是它。

宿主接口与来源校验

路由方法校验
/api/plugin-safety/checkPOST同源 Origin + 非 sec-fetch-site: cross-site
/api/plugin-safety/configGET允许无 Origin(同源 GET 浏览器不带该头),拒绝跨站标记或 Origin 不符
/api/plugin-safety/configPOST同源 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.fetchmodule.registerHooks(把 @deepseek-ai/schemastery 映射到 scripts/fixtures/)完成,不联网也能覆盖 npm/GitHub 两条解析链路与宿主路由。