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.
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-uiand 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