DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Ssrf Guard — DSH Plugin for DeepSeek Harness
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins
S

dsh-ssrf-guard

Ssrf Guard

DeepSeek Harness plugin: a fail-closed URL host and scheme allowlist checked before a request is opened, exposed as assertUrlAllowed and the ssrf_check tool

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:jwilson411/dsh-ssrf-guard#51d863358223b5a83630ed9c277a1049cd8951e0
READMECompatibilityVersions

Compatibility and provenance

Ssrf Guard is published as dsh-ssrf-guard and currently resolves to version 0.1.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
any
Release source
github
Registry updated
8/30/2026

Versions

0.1.0stable
8/30/2026

Related plugins

Loading related plugins…

Latest
0.1.0
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
any
License
MIT
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
8/30/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in security-access.

Doctor@linxin666/dsh-doctorTransactional rescue mode for DSH profiles with a supervised launcher, isolated recovery capsule, deterministic repairs, health monitoring, and a local Web recovery consolePocketdsh-pocketPut DeepSeek Harness in your pocket: one package, one settings page, and scan a QR code on your phone to access DSH on your computer in sync (LAN + public network, real-time screen mirroring).DSCODE@toddzheng024/dscode-bundleA complete DeepSeek coding agent with persistent shell, Ultra collaboration and automatic permission review.Auto Reviewdsh-auto-reviewSecond-model AI auto-review for DeepSeek Harness approval requests: a read-only reviewer subagent decides allow/deny on the approval answerer chain, with fail-closed fallback and full session-log audit.

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.