Skip to content

Weak same-origin guard in dev-server /api/* endpoints can be bypassed with forged Referer substring #119

@rspiegel-nc

Description

@rspiegel-nc

The dev server now blocks unauthenticated /api/* access unless isSameOrigin(req) passes, but the current implementation is too weak:

function isSameOrigin(req) {
  const origin = req.headers.origin || req.headers.referer || '';
  return origin.includes(`localhost:${PORT}`) || origin.includes(`127.0.0.1:${PORT}`);
}

This trusts any Origin or Referer string that merely contains localhost:PORT or 127.0.0.1:PORT as a substring. It does not parse and compare the actual origin.

As a result, a foreign header such as:

Referer: https://evil.example/?next=http://127.0.0.1:8000/

is incorrectly accepted as same-origin.

Why this matters

This is not a strict same-origin check. It is a substring check on attacker-controlled header content.

That means /api/* endpoints are still reachable if the request carries a misleading Referer or Origin value containing the local dev URL somewhere inside the string.

Even if ordinary browser fetch() from unrelated sites may now be blocked in many cases, the server-side trust decision is still wrong and should be fixed. The current implementation is fragile and can be bypassed by forged headers or non-browser clients.

Confirmed reproduction

Server running at:

http://127.0.0.1:8000/

1. No headers: correctly blocked

curl -i "http://127.0.0.1:8000/api/check-url?url=https://example.com"

Response:

HTTP/1.1 403 Forbidden
...
Forbidden

2. Forged foreign Referer containing local URL substring: incorrectly allowed

curl -i "http://127.0.0.1:8000/api/check-url?url=https://example.com" \
  -H "Referer: https://evil.example/?next=http://127.0.0.1:8000/"

Response:

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: *
...

{"status":0,"error":"unable to get local issuer certificate"}

The upstream TLS error is unrelated. The important part is that the request was accepted and forwarded instead of returning 403.

3. Same bypass works on /api/proxy

curl -i -X POST "http://127.0.0.1:8000/api/proxy" \
  -H "Content-Type: application/json" \
  -H "Referer: https://evil.example/?next=http://127.0.0.1:8000/" \
  --data '{"url":"https://example.com","method":"GET","headers":{}}'

Response:

HTTP/1.1 502 Bad Gateway
Content-Type: application/json
Access-Control-Allow-Origin: *
...

{"error":"Upstream error: unable to get local issuer certificate"}

Again, the upstream certificate failure is not the issue. The issue is that the request bypassed the same-origin guard and reached the proxy logic.

Expected behavior

The server should only accept requests whose actual origin matches the local dev origin exactly.

Examples that should be accepted:

  • http://127.0.0.1:8000
  • http://localhost:8000

Examples that should be rejected:

  • https://evil.example/?next=http://127.0.0.1:8000/
  • https://127.0.0.1:8000.evil.example
  • any unrelated origin containing the local URL as a substring

Suggested fix

Parse and compare exact origins instead of using substring matching.

For example:

function isSameOrigin(req) {
  const allowed = new Set([
    `http://127.0.0.1:${PORT}`,
    `http://localhost:${PORT}`,
  ]);

  if (req.headers.origin) {
    return allowed.has(req.headers.origin);
  }

  if (req.headers.referer) {
    try {
      return allowed.has(new URL(req.headers.referer).origin);
    } catch {
      return false;
    }
  }

  return false;
}

It would also be reasonable to rely only on Origin for API endpoints and avoid Referer fallback entirely unless there is a strong compatibility reason.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions