Back to home@Vladimir-Kryshchenko

dsh-route-fence-linter

Static linter for DeepSeek Harness plugin HTTP routes: every webServer route bypasses the /api gateway's trust check and must pin the Host to loopback itself. PASS/WARN/FAIL per route, as a CLI and a dsh tool.

Stars
0
Language
TypeScript
Created
Aug 21, 2026
Updated
Aug 21, 2026
GitHub repo

Introduction

dsh-route-fence-linter

Every plugin route registered on webServer is dispatched by longest-prefix match, ahead of the host's /api gateway — so it never passes the gateway's unified trust check. Each plugin author must ship their own browser-trust fence, and most do not. This linter finds the ones that did not.

Usage

dsh-route-fence scan                       # $DSH_HOME/profiles/web
dsh-route-fence scan /path/to/profile

Exit codes: 0 clean · 1 at least one FAIL · 2 usage/IO error.

Verdicts

VerdictMeaning
PASSThe handler gates on a fence that pins the Host before Origin handling.
WARNInside the fence, Origin / sec-fetch-site is read before the Host is pinned. Confirm the ordering by hand.
FAILOne of: no Host check anywhere; a fence that compares Origin to Host but never pins the Host to loopback (bypassable by DNS rebinding — the attacker controls both headers and they match); or a fence that exists but this handler never calls.

Bundles whose sources cannot be read are reported as SKIP and counted in the summary — a skipped bundle is not a clean bundle.

registerFallback is checked too, and reads worse than any route when unfenced: the fallback seat answers every request no named route matched, so one unfenced fallback is a wider surface than a whole prefix tree.

Scan scope

The scan covers what a package publishes (its package.json files field); a package that declares no files is scanned whole. Anything excluded is named in the output as a note line, never dropped silently.

Scoping by directory name was tried and reverted: skipping anything called tests/ or examples/ would have let a plugin hide an unfenced route in a directory with that name and be reported clean. Publication scope cannot be gamed that way — the loader can only import what was installed.

The rebinding-bypassable shape is graded FAIL, not WARN: it was found live in a real, widely-installed plugin and confirmed exploitable against a running profile — a request with Host: evil.example and Origin: http://evil.example passed the check and executed a state-changing method.

The fence this checks for

The shape the host itself uses (and dsh-better-sidebar/src/trust-fence.ts copies): pin the Host header to loopback or a configured trusted authority first, refuse cross-site fetch markers, and only then compare Origin. Comparing Origin before pinning the Host is not a fence.

Limits

Heuristic over source text, not dataflow analysis. A fence is identified as the smallest function body that both references the Host and constrains it; a handler counts as gated when it calls that fence, receives it as a value, or performs the check inline. This recognises the reference implementation, the threaded-fence pattern, and helper-based Host reads — but a sufficiently indirect fence can still be missed. Report false verdicts; they are bugs.