Skip to content

fix(auth): stop reflecting arbitrary origins in credentialed CORS - #5127

Merged
pedrofrxncx merged 1 commit into
mainfrom
fix/cors-reflects-any-origin-w6
Jul 23, 2026
Merged

fix(auth): stop reflecting arbitrary origins in credentialed CORS#5127
pedrofrxncx merged 1 commit into
mainfrom
fix/cors-reflects-any-origin-w6

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Closes #3757 ("Gateway CORS reflects any origin with credentials enabled").

The global CORS middleware in apps/mesh/src/api/app.ts reflected the incoming Origin header for every request (the origin callback had a literal // TODO: Configure allowed origins from environment and then returned the origin unconditionally), while credentials: true was set. That combination lets any external website issue a cookie-authenticated cross-site fetch(..., {credentials: 'include'}) against the API and read the JSON response for a logged-in victim — a classic permissive-CORS-with-credentials hole (CWE-942). The existing localhost/127.0.0.1 allowance was also a loose substring check (origin.includes("localhost")), bypassable by a hostname like http://localhost.evil.com.

A maintainer wants this fixed because it's a real, currently-exploitable cross-tenant data-leak vector on every session-authenticated API route (chat, connections, org settings, etc.) — not just OAuth-proxy, but the same middleware also guards the OAuth/auth-adjacent routes I was auditing.

The fix extracts the origin decision into a small pure function, resolveCorsOrigin(origin, { baseUrl, requestOrigin }), unit-tested the same way isSsoExemptPath already is in this file:

  • Reflects localhost/127.0.0.1 only on an exact hostname match (not substring).
  • Reflects the deployment's configured baseUrl origin, falling back to the request's own origin when baseUrl isn't configured — mirroring the exact fallback pattern (getSettings().baseUrl ?? reqUrl.origin) already used a few dozen lines above for redirect_uri validation in the same file.
  • Otherwise returns null (no Access-Control-Allow-Origin header), which only blocks genuinely cross-origin browser reads — same-origin app traffic (the normal case, since the Studio client and API are served from the same origin in production) is unaffected because browsers don't gate same-origin requests on CORS headers at all.

Net change: apps/mesh/src/api/app.ts (+~40/-9), apps/mesh/src/api/app.test.ts (+6 test cases covering the localhost exact-match, the substring bypass, baseUrl reflection, the no-baseUrl fallback, and rejection of an arbitrary origin).

To verify: bun test apps/mesh/src/api/app.test.ts (10/10 pass, including the 6 new resolveCorsOrigin cases).

Locally ran: bun run fmt (clean) and bunx tsc --noEmit in apps/mesh (no errors introduced by this change — the only errors present are pre-existing, in an unrelated untracked file). Full CI validates the rest.


Summary by cubic

Stops reflecting arbitrary origins when CORS uses credentials, closing a data leak where any site could read cookie-authenticated API responses. Only trusted origins are allowed; same-origin production traffic is unaffected.

  • Bug Fixes
    • Added resolveCorsOrigin to gate CORS origin when credentials: true.
    • Allow only exact localhost/127.0.0.1 (any port) and the deployment origin from getSettings().baseUrl (fallback to the request’s origin if unset).
    • Removed substring checks; reject localhost.evil.com, arbitrary sites, and malformed origins.
    • Wired the global CORS middleware to resolveCorsOrigin and added targeted tests in apps/mesh/src/api/app.test.ts.

Written for commit 92e3d99. Summary will update on new commits.

Review in cubic

The global CORS middleware reflected the incoming Origin header for
every request while credentials:true was set, so any external site
could issue a cookie-authenticated cross-site fetch against the API
and read the response. The localhost/127.0.0.1 allowance was also a
loose substring check bypassable by a hostname like
"localhost.evil.com".

resolveCorsOrigin now only reflects localhost/127.0.0.1 (exact
hostname match) and the deployment's own origin (baseUrl setting,
falling back to the request's own origin when unconfigured — the
same fallback already used for redirect_uri validation in this file).
@pedrofrxncx
pedrofrxncx merged commit 85fa7a1 into main Jul 23, 2026
15 checks passed
@pedrofrxncx
pedrofrxncx deleted the fix/cors-reflects-any-origin-w6 branch July 23, 2026 19:50
decocms Bot pushed a commit that referenced this pull request Jul 23, 2026
PR: #5127 fix(auth): stop reflecting arbitrary origins in credentialed CORS
Bump type: patch

- decocms (apps/mesh/package.json): 4.111.0 -> 4.111.1

Deploy-Scope: server
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.

[Bug]: Gateway CORS reflects any origin with credentials enabled

1 participant