DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Ssrf Guard — DeepSeek Harness 插件(DSH Plugin)
← Plugins
S

dsh-ssrf-guard

Ssrf Guard

DeepSeek Harness 插件:在打开请求前检查 URL 主机和方案的默认拒绝白名单,并通过 assertUrlAllowed 和 ssrf_check 工具提供

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:jwilson411/dsh-ssrf-guard#51d863358223b5a83630ed9c277a1049cd8951e0
README兼容性版本

兼容性与来源证明

Ssrf Guard 以 dsh-ssrf-guard 发布,当前版本为 0.1.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
any
发布来源
github
Registry 更新时间
2026/8/30

版本

0.1.0stable
2026/8/30

相关插件

正在加载相关插件…

最新版
0.1.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
any
许可证
MIT
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/8/30
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录

相关插件

继续浏览 security-access 分类下经过校验的插件。

Doctor@linxin666/dsh-doctorDSH 配置档案的事务性救援模式,配备受监督的启动器、隔离的恢复容器、确定性修复、健康监控以及本地 Web 恢复控制台Pocketdsh-pocket把 DeepSeek Harness 装进你的口袋:一个包、一个设置页,手机扫码即同步访问电脑上的 DSH(局域网 + 公网,实时同屏)。DSCODE@toddzheng024/dscode-bundle完整的 DeepSeek 编码代理,支持持久化 shell、Ultra 协作和自动权限审查。Auto Reviewdsh-auto-review针对 DeepSeek Harness 审批请求的第二模型 AI 自动审查:只读审查子代理在审批应答链上决定允许或拒绝,并采用故障关闭回退机制和完整的会话日志审计。

README

dsh-ssrf-guard

A DeepSeek Harness function plugin holding outbound URLs to a fail-closed host and scheme allowlist, checked before a request is opened. Hosts that are not on the list never leave the box, because nothing here opens anything: the check is a URL parse and a string match.

The scope is one question: may this URL be fetched? The answer is either a small structured record or a thrown error carrying code: 'SSRF_DENIED' and a reason token. Default configuration allows nothing — you name the hosts you actually need.

What it is not

This is a URL-host allowlist. It is not DNS-rebinding protection, and it is not a WAF.

  • Not DNS-rebinding protection. The hostname is matched as written in the URL, never after resolution. An allowed name whose DNS answer points at 127.0.0.1 — or that answers differently on the second lookup than on the first — is allowed by this plugin, because this plugin never looks it up. If you need that, you need a resolver-level or connect-level control, which is a different thing living in a different layer.
  • Not a WAF. Nothing inspects a request body, a response, a header, or a payload. There is no rule set, no signature list, and no traffic to filter.
  • Not a reverse proxy. It sits in your process as a function you call, not in front of your service as a hop.
  • Not an interceptor. The pinned release candidate 0.1.1-rc.2 exposes no HTTP or fetch seam, so the plugin does not invent one and does not silently wrap your client. It gives you an assertion to call at your own egress point, and a tool the model can call before it asks for a fetch.

An allow verdict is permission to try, not a promise that the host is safe.

Install

dsh plugin --profile web add github:jwilson411/dsh-ssrf-guard

dsh plugin forwards to pnpm inside $DSH_HOME/profiles/web, then reconciles the profile: because this package's manifest declares dsh.bundle.patch, it is appended to the profile manifest's ordered dsh.profile.bundles list and its cordis.patch.yml becomes a layer. Remove it the same way, with remove in place of add.

Pinned DSH release candidate

This package is written and tested against the pinned release candidate 0.1.1-rc.2 — @deepseek-ai/dsh-tools@0.1.1-rc.2 is pinned exactly in devDependencies so tests run against one known API, and the peer range is ^0.1.1-rc.2, matching how the harness's own tool packages declare it.

Note that @deepseek-ai/dsh-tools's npm latest tag still points at the older 0.0.1-rc.1; the 0.1.1-rc.2 line is published under next. Pin explicitly rather than relying on the tag.

What it registers

Cordis plugin idssrf-guard (the row id in cordis.patch.yml)
Injectstools — a hard dependency; the plugin waits rather than degrading
Toolssrf_check
Argumentsurl (string, required)

On an allow the tool returns { ok: true, url, host, scheme, plugin }, shaped by its declared output schema. On a denial it throws rather than returning ok: false — a guard a caller can walk past by forgetting to read a boolean is not a guard.

The library API

The tool is a thin wrapper. The function is the product:

import { assertUrlAllowed, SsrfDeniedError } from 'dsh-ssrf-guard'

const config = { allowHosts: ['.example.com'], allowSchemes: ['https'] }

try {
  const { host, scheme } = assertUrlAllowed(candidate, config)
  await fetch(candidate) // your call, at your own egress point
} catch (error) {
  if (error instanceof SsrfDeniedError) {
    log.warn({ code: error.code, reason: error.reason, host: error.host })
    return
  }
  throw error
}

Call it before you build the request. A non-throwing form, checkUrl(url, config), returns { ok: false, reason, url, host, scheme, message } instead for a caller that would rather branch on a value.

Config

keytypedefault
allowHostsstring[][]exact hostnames and leading-dot suffix rules. Empty allows nothing.
allowSchemesstring[]["https"]allowed URL schemes, with or without the colon

Set them from the profile's own cordis.patch.yml — note that an id-targeted patch replaces the row's whole config, so restate every field you mean to keep:

- id: ssrf-guard
  config:
    allowHosts:
      - .example.com
      - api.vendor.test
    allowSchemes: [https]

Resolution order is patch config, then environment, then default: a patch row is the deployment's stated intent, so it is not silently overridden by an ambient variable. The environment fallbacks are DSH_SSRF_ALLOW_HOSTS and DSH_SSRF_ALLOW_SCHEMES, each a comma-separated list, and each used only when the patch row omits the key entirely. An explicitly empty list is a stated intent and wins.

How a host entry matches

entrymatchesdoes not match
example.com (exact)example.comevil.example.com, notexample.com
.example.com (suffix)foo.example.com, a.b.example.com, example.comnotexample.com, example.com.evil.test
  • Comparison is case-insensitive, since DNS is: EXAMPLE.com matches example.com.
  • A trailing root dot is normalized away, so https://example.com./ cannot slip past an example.com entry as a different string.
  • IPv6 brackets are normalized away, so a config may write ::1 or [::1].
  • Ports are not part of the match. https://example.com:8443/ is allowed by example.com. If you need port control, that is a different layer.
  • A non-string or blank entry is dropped rather than coerced: a malformed allowlist shrinks towards denying, never grows towards allowing.

What is denied by default

With no config at all, everything is. With hosts configured, the following are still denied unless you name them:

deniedreason
any host not matched by allowHostsHOST_DENIED
any scheme outside allowSchemes — http: under the https-only default, and file:, data:, gopher:, ftp: always unless listedSCHEME_DENIED
loopback and unspecified addresses: 127.0.0.0/8, localhost and *.localhost, ::1, 0.0.0.0, ::LOOPBACK_DENIED
link-local, including the cloud metadata service at 169.254.169.254: 169.254.0.0/16, fe80::/10LINK_LOCAL_DENIED
a URL carrying credentials, such as https://example.com@evil.test/CREDENTIALS_DENIED
a relative or unparseable stringINVALID_URL
a URL that parses but carries no host, such as mailto:HOST_MISSING

Loopback and link-local addresses are reachable only through an exact entry. A suffix rule never unlocks them: .localhost does not admit localhost, and .254 does not admit the metadata address. Naming 127.0.0.1 in allowHosts does — that case is a local development server, and it should have to be written down.

The error

error instanceof SsrfDeniedError
error.code    // 'SSRF_DENIED' — always, for every denial
error.reason  // 'HOST_DENIED' | 'SCHEME_DENIED' | 'LOOPBACK_DENIED' |
              // 'LINK_LOCAL_DENIED' | 'CREDENTIALS_DENIED' |
              // 'HOST_MISSING' | 'INVALID_URL'
error.url     // the input string, exactly as handed in
error.host    // the normalized host, or null if parsing never got that far
error.scheme  // the scheme without its colon, or null likewise
error.message // human-readable, naming what was allowed

Branch on code and reason; the message is for a human reading a log. The full reason set is exported as REASONS.

Layout

package.json          manifest + `dsh.bundle.patch` — what makes this a bundle
cordis.patch.yml      the bundle's patch layer: one insert, one plugin row
src/ssrf.js           the pure half: normalize, match, allow or throw
src/index.js          the plugin: `name`, `inject`, `apply(ctx, config)`, the tool
test/                 offline tests: URL parses, a stub context, a hygiene scan
package-lock.json     the pinned dependency tree `npm ci` installs in CI

Tests

npm install
npm test

Offline by construction, and not merely by intention: every case is a URL parse, apply is handed a stub context that records registrations, and the tool is driven through the same execute the registry calls, with results validated against the real @deepseek-ai/dsh-tools pinned to 0.1.1-rc.2. No profile boots, no name is resolved, no socket opens, and no key is read.

test/hygiene.test.js asserts the claim rather than restating it: it scans src/ for any resolver, socket, HTTP, or dynamic-import construct and fails if one appears, checks that the only non-relative import is the pinned tools package, and scans the whole tree for machine names, mount paths, and credential shapes.

CI (.github/workflows/ci.yml) runs npm ci and npm test on Node 22 and 24 from the committed lockfile, against the public registry only. It needs no credentials and the suite reaches no network.

Out of scope

  • DNS-rebinding defence. Covered above: the name is checked as written, not as resolved.
  • A reverse proxy or a cloud WAF. Neither is here, and this is not a smaller version of one.
  • Opening sockets. The package makes no connection of any kind, including to validate that an allowed host exists.
  • Scanning networks. There is no probing, no sweeping, and no discovery.
  • IP-range policy beyond loopback and link-local. Private ranges such as 10.0.0.0/8 are denied by the allowlist being an allowlist, not by a special case. If you want them reachable, name them.
  • Deciding for you. The tool answers about one URL. What your code does with an allow is your code's business.

License

MIT — see LICENSE.