Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, string | undefined>): 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[] = []
Expand Down
27 changes: 27 additions & 0 deletions src/__tests__/server/serverConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([])
})
Expand Down