From 852af47293ec1e839e1a6a29d564ab30fef95dee Mon Sep 17 00:00:00 2001 From: lovepixel-git Date: Sat, 1 Aug 2026 23:32:16 -0400 Subject: [PATCH] fix(server): let platform auto-detection survive an invalid PUBLIC_ORIGIN `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 #185 --- server/config.ts | 13 ++++++++--- src/__tests__/server/serverConfig.test.ts | 27 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/server/config.ts b/server/config.ts index 3a73208c4..1e9ae55a2 100644 --- a/server/config.ts +++ b/server/config.ts @@ -59,17 +59,24 @@ function normalizeOrigins(raw: readonly string[]): string[] { * * Precedence: * 1. `PUBLIC_ORIGIN` — comma-separated list (platform domain + custom domain - * can coexist). Invalid entries are dropped. + * can coexist). Invalid entries are dropped; a list that survives + * normalization with at least one entry wins outright. * 2. Platform auto-detection — `RENDER_EXTERNAL_URL` (full URL) and/or * `https://${RAILWAY_PUBLIC_DOMAIN}` (host only). Both are included when * both env vars are present, keeping one-click deploys config-free. + * Reached when `PUBLIC_ORIGIN` is unset *or* every entry in it is + * unparseable, so a malformed value degrades to auto-detection rather + * than suppressing it. * 3. `[]` — no public origin configured; the CSRF check falls back to the * inbound `Host` header. */ export function resolvePublicOrigins(env: Record): string[] { - const explicit = readCsvList(env.PUBLIC_ORIGIN) + // Normalize before the precedence test, not after: gating on the raw CSV + // length lets an all-invalid PUBLIC_ORIGIN return [] while still claiming + // precedence, which suppresses platform auto-detection entirely. + const explicit = normalizeOrigins(readCsvList(env.PUBLIC_ORIGIN)) if (explicit.length > 0) { - return normalizeOrigins(explicit) + return explicit } const derived: string[] = [] diff --git a/src/__tests__/server/serverConfig.test.ts b/src/__tests__/server/serverConfig.test.ts index eab41dae1..fb467f4c8 100644 --- a/src/__tests__/server/serverConfig.test.ts +++ b/src/__tests__/server/serverConfig.test.ts @@ -85,6 +85,33 @@ describe('resolvePublicOrigins', () => { ).toEqual(['https://www.example.com']) }) + it('falls back to platform vars when every PUBLIC_ORIGIN entry is invalid', () => { + // A one-click template that renders `https://${RAILWAY_PUBLIC_DOMAIN}` + // before the domain exists yields the bare scheme. Gating precedence on + // the raw CSV instead of the normalized result made that value suppress + // auto-detection, leaving the CSRF check with no configured origin — i.e. + // setting PUBLIC_ORIGIN badly was worse than not setting it at all. + expect( + resolvePublicOrigins({ + PUBLIC_ORIGIN: 'https://', + RAILWAY_PUBLIC_DOMAIN: 'app.up.railway.app', + }), + ).toEqual(['https://app.up.railway.app']) + }) + + it('falls back to platform vars when PUBLIC_ORIGIN is only separators', () => { + expect( + resolvePublicOrigins({ + PUBLIC_ORIGIN: 'not-a-url, also-bad', + RENDER_EXTERNAL_URL: 'https://app.onrender.com', + }), + ).toEqual(['https://app.onrender.com']) + }) + + it('returns [] when PUBLIC_ORIGIN is invalid and no platform var is set', () => { + expect(resolvePublicOrigins({ PUBLIC_ORIGIN: 'https://' })).toEqual([]) + }) + it('returns [] when nothing is configured', () => { expect(resolvePublicOrigins({})).toEqual([]) })