mward4 553417c613 dsh-disable-untrusted-origin 0.1.0
Treat every browser window as if it were running on the DeepSeek Harness
host itself.

The harness fences its HTTP surface by request identity: the /api gateway
accepts a Host that is loopback or an operator-declared authority, and each
plugin repeats a check of its own. A window that arrives by a LAN address,
a DNS name, or a tunnel therefore authenticates normally and then fails
every mutating request with 403, which reads to the user as a button that
does nothing.

The plugin removes that distinction in two places:

- A request/upgrade listener prepended on the raw node:http server the
  webserver owns, ahead of the handler it was constructed with. Every
  connection is answered with the loopback authority and a matching
  Origin, so one interception covers the core gateway and every plugin
  that fences on Host, including versions published before the harness
  grew a declared-authority list.

- The browser-side isLoopback flag, granted through the harness's own
  webserver/index-inject channel with the same ownsHost marker the
  packaged Desktop build publishes, rather than by string-replacing a
  minified client bundle.

disableAuth (default true) also removes the browser-session check, so no
process token is needed and reachability becomes the only access control.
spoofSocketAddress (default false) additionally satisfies fences that read
request.socket.remoteAddress.
2026-09-27 05:28:18 -04:00
2026-09-27 05:28:18 -04:00
2026-09-27 05:28:18 -04:00
2026-09-27 05:28:18 -04:00
2026-09-27 05:28:18 -04:00
2026-09-27 05:28:18 -04:00

dsh-disable-untrusted-origin

Treat every browser window as if it were running on the DeepSeek Harness host itself.

A window that reaches the harness by a LAN address, a DNS name, or a tunnel authenticates normally and then fails every mutating request with 403 {"error":"untrusted origin"}. Cards render, but Install, Update, toggle, and "Update all" do nothing that the user can see. This plugin removes the distinction between a local window and a remote one. No token, no --trusted-host, no per-plugin configuration.


⚠️ Read this before enabling it

This plugin is a deliberate and total removal of the harness's request-identity fence. It is not a bug fix and it is not a workaround for one specific plugin.

The harness fences requests by their Host and Origin headers for one reason: those are the only headers a page on another origin cannot forge, so they are what stops a web page you happen to visit from driving your agent API while the harness is reachable. With this plugin enabled:

  • DNS rebinding against the harness API is no longer defended.
  • A page on any other origin can issue mutating requests to the API.
  • Those requests can run commands on your machine through the agent.

With disableAuth: true (the default) there is no session check either, so anyone who can reach the port has full control of the agent, including code execution. Reachability becomes the only access control.

That is a reasonable posture when the port is behind something that authenticates on your behalf, or when it is bound to a network you fully control. It is a poor posture for a 0.0.0.0 bind fronted by a public tunnel with no authentication of its own. Decide accordingly, and re-read this section if you change how the port is exposed.

If you want the errors gone but not the exposure, set disableAuth: false. That keeps the process token as the access control while still answering the identity fence, which is what actually produces the 403.


What it changes

Two things, in two different places.

1. Request identity, at the server. The plugin installs request and upgrade listeners with prependListener on the raw node:http server the webserver owns, ahead of the handler that server was constructed with. Every connection is then answered with the loopback authority and a matching Origin. Because the rewrite happens before any route is looked up, it covers the core /api gateway and every plugin that fences on Host — whatever each one's implementation looks like, and however old it is. That includes plugin versions published before the harness grew a declared-authority list, which cannot be fixed by configuration at all.

2. The browser-side isLoopback flag. @deepseek-ai/dsh-client-connection computes isLoopback in the browser as transport?.ownsHost === true || isLoopbackHostname(pageLocation.hostname). The hostname half stays false for any window that arrived by a LAN address or a tunnel, which drops the settings and model surfaces into in-memory fallback and leaves their cards empty. The plugin publishes the same ownsHost marker the packaged Desktop build publishes, using the harness's own webserver/index-inject channel — the same mechanism @deepseek-ai/dsh-web-app uses for __DSH_CONNECTION_RECOVERY__.

It deliberately does not touch sec-fetch-site. The harness refuses cross-site outright, and that refusal is the last judgement a browser makes on its own behalf. Rewriting it would make a page on another origin look exactly like a window on this machine.


Install

# from a checkout or tarball on disk
dsh plugin --profile web add file:/path/to/dsh-disable-untrusted-origin

# or from a published package
dsh plugin --profile web add dsh-disable-untrusted-origin

Then restart the host so the new bundle is loaded.

The bundle patch inserts the row automatically. Verify it loaded:

dsh plugin --profile web list | grep disable-untrusted-origin

Configuration

Override any key in your profile's cordis.patch.yml:

- id: disable-untrusted-origin
  config:
    enabled: true
    disableAuth: true
    forceClientLoopback: true
    spoofSocketAddress: false
Key Default Effect
enabled true Master switch. false loads the plugin and changes nothing.
disableAuth true Also removes the browser-session check, so no process token is needed. This is the one that removes access control.
forceClientLoopback true Grants the browser-side isLoopback flag so the settings and model surfaces act as they do on the host.
spoofSocketAddress false Also reports the peer address as loopback. See below.

spoofSocketAddress

A minority of plugins fence on request.socket.remoteAddress rather than on Host. No header rewrite can satisfy those; the plugin has to report the peer address as loopback. The skill explorer (@linxin666/dsh-client-ui-skill-explorer) is the one shipped today — over a LAN or a tunnel its routes answer 403 forbidden: loopback-only, and its skill cards do not work until this is enabled.

The socket is reused across keep-alive requests, so this is a property of the connection, not the request, and it is a lie every consumer of the peer address will believe: a reverse-proxy plugin's client-IP attribution, an access log, or a ban list. If you also run @goodandready/dsh-lanmode, leave this off — lanmode's device registry, per-device revocation, and IP bans all read the peer address, and this makes them see every client as the same machine.


Compatibility

Verified against DSH 0.1.7-rc.2. The plugin depends on three things that are internal rather than public API, so a harness release that renames any of them will break it:

Depends on Where
webServer.server — the raw node:http server @deepseek-ai/dsh-host-webserver
connection.requestRejection / authorizeIndex / authenticatedUrl @deepseek-ai/dsh-client-connection
the webserver/index-inject channel and the ownsHost marker webserver + connection client

If webServer.server is ever absent the plugin throws at load with a clear message rather than failing quietly.

How this differs from @goodandready/dsh-lanmode

lanmode solves the same problem from outside: it opens a second listener on its own port and proxies to the harness loopback port, rewriting Host and Origin on the way through. That is more robust — it does not depend on harness internals — but it needs a second port, it only applies to traffic that goes through it, and in mode: auto it declines to open that listener at all when something already answers on the harness port, which is what a 0.0.0.0 bind looks like from the inside.

This plugin solves it from inside, on the port you already use, with no second listener. The two are not mutually exclusive, but running both is redundant and spoofSocketAddress will confuse lanmode's device tracking.

What it does not fix

  • Plugin routes that require a paired device rather than a trusted origin (@linxin666/dsh-remote-web-ui and anything keying on its pairing service). Those authorize an identity, not an origin, so no header rewrite reaches them.
  • Anything gating on TLS state, or on a proxy's own authentication.

Verification

node test/fence.test.mjs   # drives real requests past the real plugin-market fence
node test/apply.test.mjs   # wiring, including that unload restores every original

test/fence.test.mjs loads sameOrigin() from the installed dshmarket when a profile is present and says which source it used, so a run against a bundled copy is never mistaken for a run against the real thing. Both files need no dependency tree beyond a resolvable @deepseek-ai/schemastery for the second.

License

MIT

S
Description
Removes the untrusted-origin restriction from DeepSeek Harness: one interceptor answers every request with the loopback authority.
Readme MIT
297 KiB
0 Stars 1 Watchers 0 Forks
Languages
JavaScript 100%