Skip to content

fix(security): tighten /_proxy/mode origin check (CSRF / DNS rebinding)#16

Open
aaronjmars wants to merge 1 commit into
aattaran:mainfrom
aaronjmars:security/origin-check-csrf
Open

fix(security): tighten /_proxy/mode origin check (CSRF / DNS rebinding)#16
aaronjmars wants to merge 1 commit into
aattaran:mainfrom
aaronjmars:security/origin-check-csrf

Conversation

@aaronjmars
Copy link
Copy Markdown

Summary

proxy/model-proxy.js exposes a control endpoint at POST /_proxy/mode that swaps the local proxy's active backend (DeepSeek / OpenRouter / Fireworks / Anthropic). The CSRF defense is an Origin-header allowlist:

const origin = clientReq.headers['origin'] || '';
if (origin && !origin.startsWith('http://127.0.0.1') && !origin.startsWith('http://localhost')) {
    clientRes.writeHead(403, { 'content-type': 'application/json' });
    ...
}

String.prototype.startsWith has no boundary, so http://localhost.attacker.com:3200 and http://127.0.0.1.attacker.com:3200 both pass the check. Combined with DNS rebinding — attacker registers a domain like localhost.attacker.com, hosts the page on port 3200, then re-points the A record to 127.0.0.1 — a victim browsing the attacker page can fetch('/_proxy/mode', {method:'POST', body:'backend=...'}) and silently switch the user's active backend.

Impact

  • Silent prompt redirection. The proxy attaches the user's stored API key for the active backend before forwarding /v1/messages. After a forced switch, prompts the user composes against (e.g.) Anthropic are sent to DeepSeek with valid auth — proprietary code, secrets in scrollback, and conversation context follow.
  • Cost asymmetry. Forcing a victim from DeepSeek to Anthropic is a 17× output-token bill spike (the README's headline number, in reverse).
  • No attacker creds required — they flip whichever backend the user already configured.

Location

proxy/model-proxy.js:230-235 (against main @ 70518b6).

Fix

Replace the prefix check with a strict regex anchored to the loopback host with optional port:

const LOCAL_ORIGIN_RE = /^http:\/\/(?:127\.0\.0\.1|localhost|\[::1\])(?::\d{1,5})?$/i;
...
if (origin && !LOCAL_ORIGIN_RE.test(origin)) { ... }

Curl/CLI usage (no Origin header) continues to work, matching the documented usage in the README.

Verification

End-to-end against a local instance of startModelProxy on port 13200:

Origin header Before patch After patch
(none — curl) 200, switch 200, switch
http://localhost:13200 200, switch 200, switch
http://127.0.0.1 200, switch 200, switch
http://localhost.attacker.com:13200 200, switch 403
http://127.0.0.1.attacker.com:13200 200, switch 403
http://attacker.com 403 403

Preserves all legitimate paths (curl, slash-command flow, VS Code task POSTs from loopback) and closes both DNS-rebinding bypass shapes.

Detected by

Aeon (manual review of proxy/model-proxy.js after Semgrep / TruffleHog / OSV came back clean).

  • Severity: medium
  • CWE-352 (CSRF) + CWE-350 (reverse-DNS reliance)

Filed by Aeon. Close it if you'd rather fix this differently.

The origin allowlist on the /_proxy/mode endpoint used String.startsWith
to match `http://127.0.0.1` and `http://localhost`. Because startsWith
has no boundary, an Origin like `http://localhost.attacker.com:3200` or
`http://127.0.0.1.attacker.com:3200` also passed.

Combined with DNS rebinding (attacker registers `localhost.attacker.com`,
serves a page from port 3200, then rebinds the A record to 127.0.0.1),
a victim browsing the attacker page can POST to /_proxy/mode and
silently switch the local proxy's backend.

Impact:
- Prompts intended for one provider are silently routed to another
  (e.g., user thinks Anthropic, attacker switches to DeepSeek →
  proprietary code/secrets leak to a different vendor).
- Cost: forcing a switch from DeepSeek to Anthropic is a 17x bill spike.
- The proxy attaches the user's stored API key for the active backend,
  so the switched-in vendor receives valid auth.

Replace the prefix check with a strict regex that anchors the host
exactly (127.0.0.1, localhost, or [::1]) with optional port. Curl/CLI
callers that omit the Origin header continue to work unchanged.

Detected by Aeon (manual review of proxy/model-proxy.js).
Severity: medium. CWE-352 (CSRF) + CWE-350 (reverse-DNS reliance).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant