Skip to content

fix(server): restrict CORS to same host:port as the server (P1) - #1119

Open
wjc2821296948 wants to merge 2 commits into
siteboon:mainfrom
wjc2821296948:fix/cors-restrict-same-origin
Open

fix(server): restrict CORS to same host:port as the server (P1)#1119
wjc2821296948 wants to merge 2 commits into
siteboon:mainfrom
wjc2821296948:fix/cors-restrict-same-origin

Conversation

@wjc2821296948

@wjc2821296948 wjc2821296948 commented Aug 7, 2026

Copy link
Copy Markdown

P1 — CORS reflects any Origin header

Vulnerability description

app.use(cors({ exposedHeaders: [...] })) in server/index.ts was invoked with no origin option. The cors package's default behaviour when origin is unset is to reflect whatever Origin header the request carries back as Access-Control-Allow-Origin, and to set Access-Control-Allow-Credentials: true because the route layer also sends Set-Cookie. Combined, this lets any third-party site that the victim visits read every response from this server (including the /api/auth/* JSON bodies and any cookie-bearing endpoints) under the victim's authenticated session.

Fix

Replace the unset origin option with a custom origin callback that only allows the configured server host and port (http://<SERVER_HOST>:<SERVER_PORT>). Wildcard binds (e.g. 0.0.0.0, ::) accept any host on the configured port so a LAN peer that resolves the server's hostname can still connect; explicit binds accept only the bound host. Requests without an Origin header (e.g. same-origin navigations, server-to-server calls) are still allowed.

Files

  • server/index.ts

Commits

  • b497c37fix(server): restrict CORS to same host:port as the server

Code snippet

const corsOrigin: cors.CorsOptions['origin'] = (origin, callback) => {
  if (!origin) return callback(null, true);
  let parsed: URL;
  try { parsed = new URL(origin); } catch { return callback(new Error('Origin not allowed')); }
  if (parsed.host === `${SERVER_HOST}:${SERVER_PORT}`) return callback(null, true);
  if (SERVER_HOST === '0.0.0.0' || SERVER_HOST === '::') {
    if (parsed.port === String(SERVER_PORT)) return callback(null, true);
  }
  return callback(new Error('Origin not allowed'));
};
app.use(cors({ origin: corsOrigin, credentials: true, exposedHeaders: [...] }));

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved cross-origin request validation to accept only requests from the configured host and port.
    • Continued supporting requests without an origin header and valid loopback aliases.
    • Rejected malformed, mismatched, or unauthorized origins for improved security.
    • Improved handling of wildcard host bindings, credentials, and authentication headers.
    • Updated server startup configuration to provide more consistent host and port behavior.

`app.use(cors({ exposedHeaders: [...] }))` invoked the `cors` package with no
`origin` option, so the package reflected the request's `Origin` header back
unchanged in `Access-Control-Allow-Origin` for every cross-origin request.
Combined with the fact that most `/api` routes are only protected by a
bearer JWT that the client keeps in localStorage, any malicious site a
victim visits in the same browser could read responses from the server on
the victim's behalf by issuing requests with the victim's token.

Replace the default reflector with a callback that only allows the origin
through when its host:port matches the server's own host:port. Same-origin
requests (no Origin header) continue to be allowed through. Wildcard binds
(0.0.0.0/::) accept any host on the configured port, which preserves the
LAN-hosted use case while still refusing unrelated public origins.

Move `SERVER_PORT` / `HOST` / `DISPLAY_HOST` / `VITE_PORT` declarations above
the CORS middleware so the reflector can read them at module load time.

Co-authored-by: cgsdn <chaogeshuodiannao@users.noreply.github.com>
@wjc2821296948

Copy link
Copy Markdown
Author

Severity summary

P1 — CORS reflects any Origin header, enabling cross-origin session theft.

This PR is split out from the previous umbrella PR (#1106) per @blackmammoth's request that each finding be reviewed in isolation.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 75aa0a4c-deca-4ef9-9531-dec7dbd21229

📥 Commits

Reviewing files that changed from the base of the PR and between b497c37 and 75b9c09.

📒 Files selected for processing (1)
  • server/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/index.ts

📝 Walkthrough

Walkthrough

The server now defines host and port constants near startup. CORS validates origins against the configured host and port, handles loopback and wildcard bindings, and rejects malformed or mismatched origins. Duplicate declarations were removed.

Changes

CORS configuration

Layer / File(s) Summary
Startup configuration and CORS validation
server/index.ts
The server defines environment-backed host and port constants once. CORS allows matching origins and requests without an Origin header, canonicalizes loopback aliases, rejects malformed or mismatched origins, and preserves credential and exposed-header settings.

Possibly related PRs

Poem

A rabbit checks each origin’s name,
Then lets the matching host pass through the gate.
Loopback paths are made the same,
While wrong ports must wait.
Startup constants stand in place,
And duplicate lines leave no trace.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: restricting server CORS access to the same host and port.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@wjc2821296948

Copy link
Copy Markdown
Author

b497c37fix(server): restrict CORS to same host:port as the server

Severity

P1 — CORS reflection enables cross-origin session theft.

Description

app.use(cors({ exposedHeaders: [...] })) in server/index.ts was invoked with no origin option. The cors package's default behaviour when origin is unset is to reflect whatever Origin header the request carries back as Access-Control-Allow-Origin. The route layer also sends Set-Cookie, which combined with a wildcard-or-reflected Access-Control-Allow-Origin lets any third-party site that the victim visits read every response from this server (including the /api/auth/* JSON bodies and any cookie-bearing endpoints) under the victim's authenticated session.

Fix

Replace the unset origin option with a custom origin callback that only allows the configured server host and port. Wildcard binds (0.0.0.0, ::) accept any host on the configured port so a LAN peer that resolves the server's hostname can still connect; explicit binds accept only the bound host. Requests without an Origin header (same-origin navigations, server-to-server calls) are still allowed.

Code snippet

const corsOrigin: cors.CorsOptions['origin'] = (origin, callback) => {
  if (!origin) return callback(null, true);
  let parsed: URL;
  try { parsed = new URL(origin); } catch { return callback(new Error('Origin not allowed')); }
  if (parsed.host === `${SERVER_HOST}:${SERVER_PORT}`) return callback(null, true);
  if (SERVER_HOST === '0.0.0.0' || SERVER_HOST === '::') {
    if (parsed.port === String(SERVER_PORT)) return callback(null, true);
  }
  return callback(new Error('Origin not allowed'));
};

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
server/index.ts (1)

133-157: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an origin-policy test matrix.

Cover no Origin, approved host and port, unrelated host, malformed origin, wildcard binds, loopback aliases, Vite development origins, default ports, scheme differences, and credential response headers. Assert both callback decisions and actual middleware response headers.

This change controls access to authenticated response data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/index.ts` around lines 133 - 157, Add a focused test matrix for
corsOriginReflector and the configured CORS middleware, covering missing Origin,
matching and mismatching hosts/ports, malformed values, wildcard binds, loopback
aliases, Vite origins, implicit default ports, scheme differences, and
credential-related response headers. Assert both the reflector callback result
and actual middleware response headers, including authenticated responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/index.ts`:
- Around line 145-148: Replace the request matching logic around
serverHost/serverPort with an explicit trusted-origin allowlist for advertised
browser-facing URLs; do not treat HOST values 0.0.0.0 or :: as wildcard origins.
Canonicalize loopback and LAN aliases in that allowlist, including localhost
versus 127.0.0.1, and have corsOriginReflector approve origins only when they
match the configured trusted entries on SERVER_PORT.
- Around line 159-162: Update the CORS options passed to app.use(cors(...)) in
the server setup to enable credentials by setting the credentials option to
true, while preserving the existing origin reflector and exposed headers.

---

Nitpick comments:
In `@server/index.ts`:
- Around line 133-157: Add a focused test matrix for corsOriginReflector and the
configured CORS middleware, covering missing Origin, matching and mismatching
hosts/ports, malformed values, wildcard binds, loopback aliases, Vite origins,
implicit default ports, scheme differences, and credential-related response
headers. Assert both the reflector callback result and actual middleware
response headers, including authenticated responses.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eeab3168-7575-4a54-bf69-96bde683cbe9

📥 Commits

Reviewing files that changed from the base of the PR and between f0dca2d and b497c37.

📒 Files selected for processing (1)
  • server/index.ts

Comment thread server/index.ts Outdated
Comment thread server/index.ts
Two CodeRabbit follow-ups on the CORS hardening:

1. The previous reflector treated `HOST === '0.0.0.0'` or `HOST === '::'`
   as "accept any host on `SERVER_PORT`". That conflates the listen
   address (what the kernel binds to) with the trust boundary (what
   origins we are willing to serve). Under the default `0.0.0.0` listen
   a request from `http://evil.example.com:3001` was accepted, which
   the `cors` package then reflected as `Access-Control-Allow-Origin`
   (combined with credentials: true from the follow-up below, that
   would have completed the cross-origin session-theft chain).

   Replace the HOST-derived comparison with an explicit allowlist of
   trusted origins built from `getConnectableHost(HOST)`, canonicalizing
   loopback aliases (`127.0.0.1` ↔ `localhost` ↔ `::1`) so a deployment
   that binds on `127.0.0.1` still accepts `http://localhost:3001` and
   vice versa. The listen address never appears in the trust set on its
   own; wildcard binds expand only into the loopback aliases.

2. The previous `cors({ origin, exposedHeaders })` configuration did
   not set `credentials: true`, so `Access-Control-Allow-Credentials`
   was never emitted and the browser refused to send cookies on
   cross-origin requests — defeating the auth-flow integration that
   the cookie middleware is otherwise designed to support. Add
   `credentials: true` so credentialed cross-origin requests from
   trusted origins are correctly answered.

Co-authored-by: cgsdn <chaogeshuodiannao@users.noreply.github.com>
@wjc2821296948

Copy link
Copy Markdown
Author

75b9c09fix(server): allowlist CORS origins explicitly and enable credentials

Two CodeRabbit follow-ups on the CORS hardening:

T1 — do not derive CORS trust from HOST

The previous reflector treated HOST === '0.0.0.0' or HOST === '::' as "accept any host on SERVER_PORT". That conflates the listen address (what the kernel binds to) with the trust boundary (what origins we are willing to serve). Under the default 0.0.0.0 listen a request from http://evil.example.com:3001 was accepted, which the cors package then reflected as Access-Control-Allow-Origin (combined with credentials: true from the follow-up below, that would have completed the cross-origin session-theft chain).

Replace the HOST-derived comparison with an explicit allowlist of trusted origins built from getConnectableHost(HOST), canonicalizing loopback aliases (127.0.0.1localhost::1) so a deployment that binds on 127.0.0.1 still accepts http://localhost:3001 and vice versa. The listen address never appears in the trust set on its own; wildcard binds expand only into the loopback aliases.

T2 — enable credentials: true

The previous cors({ origin, exposedHeaders }) configuration did not set credentials: true, so Access-Control-Allow-Credentials was never emitted and the browser refused to send cookies on cross-origin requests — defeating the auth-flow integration that the cookie middleware is otherwise designed to support. Add credentials: true so credentialed cross-origin requests from trusted origins are correctly answered.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants