Harden CORS origin validation in production (FRE-4749)
- Add ALLOWED_ORIGINS env var with comma-separated origin list - Validate origins at startup in production: reject wildcards, empty values, and malformed URLs (non-http/https protocol) - Update both server entry points (server.ts, index.ts) to use getCorsOrigins() - Development mode retains existing localhost fallback behavior Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -8,6 +8,7 @@ const envSchema = z.object({
|
||||
API_RATE_LIMIT_WINDOW: z.string().transform(Number).default(60000), // 1 minute
|
||||
API_RATE_LIMIT_MAX_REQUESTS: z.string().transform(Number).default(100),
|
||||
CORS_ORIGIN: z.string().default('http://localhost:5173'),
|
||||
ALLOWED_ORIGINS: z.string().default(''),
|
||||
});
|
||||
|
||||
export const apiEnv = envSchema.parse({
|
||||
@@ -17,8 +18,50 @@ export const apiEnv = envSchema.parse({
|
||||
API_RATE_LIMIT_WINDOW: process.env.API_RATE_LIMIT_WINDOW,
|
||||
API_RATE_LIMIT_MAX_REQUESTS: process.env.API_RATE_LIMIT_MAX_REQUESTS,
|
||||
CORS_ORIGIN: process.env.CORS_ORIGIN,
|
||||
ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS,
|
||||
});
|
||||
|
||||
/**
|
||||
* Parse ALLOWED_ORIGINS into a validated set.
|
||||
* In production, rejects wildcards ('*') and empty values.
|
||||
* In development, falls back to localhost.
|
||||
*/
|
||||
export function getCorsOrigins(): string | string[] {
|
||||
const origins = (apiEnv.ALLOWED_ORIGINS || '').split(',').filter(Boolean);
|
||||
|
||||
if (apiEnv.NODE_ENV === 'production') {
|
||||
if (origins.length === 0) {
|
||||
throw new Error(
|
||||
'CORS origin validation (FRE-4749): ALLOWED_ORIGINS is empty in production. ' +
|
||||
'Set ALLOWED_ORIGINS to a comma-separated list of allowed origins.'
|
||||
);
|
||||
}
|
||||
for (const origin of origins) {
|
||||
if (origin === '*') {
|
||||
throw new Error(
|
||||
'CORS origin validation (FRE-4749): wildcard (*) ALLOWED_ORIGIN in production.'
|
||||
);
|
||||
}
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
||||
throw new Error(
|
||||
`CORS origin validation (FRE-4749): invalid protocol "${url.protocol}" in "${origin}". Expected http: or https:`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.startsWith('CORS origin')) throw err;
|
||||
throw new Error(
|
||||
`CORS origin validation (FRE-4749): malformed origin "${origin}": ${err instanceof Error ? err.message : String(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return origins;
|
||||
}
|
||||
|
||||
return apiEnv.CORS_ORIGIN || 'http://localhost:5173';
|
||||
}
|
||||
|
||||
// Rate limit configuration by tier
|
||||
export const rateLimitConfig = {
|
||||
basic: {
|
||||
|
||||
Reference in New Issue
Block a user