diff --git a/src/server.ts b/src/server.ts index 3b0d948..eb7c9e0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -78,17 +78,34 @@ async function startServer() { const app = express() // Apply security headers with helmet + // Get Helmet's default CSP directives and remove upgrade-insecure-requests + // This ensures the directive is never added, even in edge cases + // Create a copy to avoid mutating Helmet's defaults + const defaultCspDirectives = { ...helmet.contentSecurityPolicy.getDefaultDirectives() } + delete defaultCspDirectives['upgrade-insecure-requests'] + + // Build custom CSP directives, overriding defaults as needed + const cspDirectives = { + ...defaultCspDirectives, + // Override default-src from defaults + 'default-src': ["'self'"], + // Override script-src for webpack + 'script-src': ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // unsafe-eval required for webpack runtime + // Override style-src for Material-UI + 'style-src': ["'self'", "'unsafe-inline'"], // Required for Material-UI + // Add WebSocket support + 'connect-src': ["'self'", 'ws:', 'wss:'], // Allow WebSocket connections + // Allow data URIs for images + 'img-src': ["'self'", 'data:', 'blob:'], + // Only add upgrade-insecure-requests if explicitly enabled via env var + ...(enableUpgradeInsecure && { 'upgrade-insecure-requests': [] }), + } + app.use( helmet({ contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], // unsafe-eval required for webpack runtime - styleSrc: ["'self'", "'unsafe-inline'"], // Required for Material-UI - connectSrc: ["'self'", 'ws:', 'wss:'], // Allow WebSocket connections - imgSrc: ["'self'", 'data:', 'blob:'], - upgradeInsecureRequests: enableUpgradeInsecure ? [] : null, // Only enable when behind HTTPS reverse proxy - }, + useDefaults: false, // Don't merge with Helmet's defaults to ensure full control + directives: cspDirectives, }, hsts: isProduction ? {