Skip to content

fix(server): let platform auto-detection survive an invalid PUBLIC_ORIGIN - #328

Open
lovepixel-git wants to merge 1 commit into
CoreBunch:mainfrom
lovepixel-git:fix/public-origin-fallback
Open

fix(server): let platform auto-detection survive an invalid PUBLIC_ORIGIN#328
lovepixel-git wants to merge 1 commit into
CoreBunch:mainfrom
lovepixel-git:fix/public-origin-fallback

Conversation

@lovepixel-git

Copy link
Copy Markdown

The defect

resolvePublicOrigins tests the precedence gate against the raw CSV list but returns the normalized one. A PUBLIC_ORIGIN whose entries all fail to parse therefore claims precedence and then resolves to [], silently suppressing platform auto-detection:

resolvePublicOrigins({ PUBLIC_ORIGIN: 'https://', RAILWAY_PUBLIC_DOMAIN: 'app.up.railway.app' })
  -> []

resolvePublicOrigins({ RAILWAY_PUBLIC_DOMAIN: 'app.up.railway.app' })
  -> ['https://app.up.railway.app']

Setting the variable badly is strictly worse than leaving it unset. normalizeOrigin returns null for 'https://' because new URL('https://') throws, so the list empties out after the gate has already been passed.

Downstream, an empty publicOrigins puts the CSRF check on its inbound-Host fallback, which derives the scheme from req.url. Behind a TLS-terminating edge that yields http://… against a browser Origin of https://…, and the request is rejected.

Change

Normalize before the gate rather than after. One function, four lines.

A list with at least one parseable entry still wins outright, and invalid entries inside a partly valid list are still dropped rather than promoting auto-detection. Both existing tests that pin those behaviors pass unchanged, which is the signal that the diff is narrow.

A design call that is yours, not mine

Given a PUBLIC_ORIGIN where every entry is unparseable, there are three defensible behaviors:

  1. Fall through to platform auto-detection (what this PR does)
  2. Throw at boot, so a malformed env var fails loudly instead of degrading quietly
  3. Warn and fall through

I picked 1 because it makes the function match its own documented precedence, and because it is the smallest change. But this is your CSRF code from #14 and you wrote the existing test that deliberately drops invalid entries, so you may well prefer 2 — a container that refuses to start is arguably better operator feedback than one that quietly resolves to something else. Say the word and I will rewrite it as a boot-time throw, or add a warning alongside the fallthrough.

On #185

I am referencing that issue as motivation, not claiming it as a fix. The code defect above is proven independently. Whether it explains that particular Railway report is a hypothesis: it fits (a template rendering https://${{RAILWAY_PUBLIC_DOMAIN}} before the domain is provisioned collapses to exactly https://), but I cannot verify Railway's variable-resolution timing, and if RAILWAY_PUBLIC_DOMAIN was also empty on that boot the fallthrough would have returned [] anyway. Treat this as a standalone correctness fix.

Tests

Three cases added to the existing describe('resolvePublicOrigins') block. Two of them fail on current main and pass here; the third (invalid PUBLIC_ORIGIN with no platform var still yields []) passes either way and is there to pin the non-over-correction.

  • bunx tsc -b exit 0
  • bun run lint clean
  • 190/190 across serverConfig, security, apiSecurityBoundary, security-headers and publicForms
  • bun test 6533 pass, 1 fail

That single failure is Bundle size budgets > ContentPage-*.js, 90043 B against a 90000 B budget. Pre-existing and unrelated: it reproduces byte-identically on untouched main and only surfaces once dist/ exists.

I deliberately did not touch expectedOrigin's scheme handling. server/auth/security.ts documents that forwarded headers are not consulted for CSRF, and that is the point of the design.

…IGIN

`resolvePublicOrigins` tested the precedence gate against the raw CSV
list but returned the normalized one, so a `PUBLIC_ORIGIN` whose entries
all fail to parse claimed precedence and then resolved to `[]`:

  resolvePublicOrigins({ PUBLIC_ORIGIN: 'https://',
                         RAILWAY_PUBLIC_DOMAIN: 'app.up.railway.app' })
    -> []
  resolvePublicOrigins({ RAILWAY_PUBLIC_DOMAIN: 'app.up.railway.app' })
    -> ['https://app.up.railway.app']

Setting the variable badly was strictly worse than leaving it unset:
platform auto-detection was suppressed, publicOrigins came back empty,
and the CSRF check fell through to its inbound-Host fallback.

Normalizing before the gate fixes it. A list with at least one parseable
entry still wins outright, and invalid entries in a partly valid list are
still dropped rather than promoting auto-detection.

Refs CoreBunch#185
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