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([]) })