From fa7c4fbec0343fee5df66f50660b14cfc799b1ec Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Tue, 11 Nov 2025 07:12:06 +0300 Subject: [PATCH] cors --- backend/src/app.js | 100 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/backend/src/app.js b/backend/src/app.js index 9ccb885..e0eec60 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -17,6 +17,7 @@ const PORT = process.env.PORT || 3000; app.set('trust proxy', true); // Security middleware with relaxed CSP for SPA +// Note: CSP is relaxed to allow assets to load properly app.use( helmet({ contentSecurityPolicy: { @@ -26,20 +27,26 @@ app.use( "'self'", "'unsafe-inline'", // Required for Vite HMR and some inline scripts "'unsafe-eval'", // Required for Vite dev mode + "https:", // Allow scripts from HTTPS sources + "http:", // Allow scripts from HTTP sources (for development) ], styleSrc: [ "'self'", "'unsafe-inline'", // Required for inline styles + "https:", // Allow styles from HTTPS sources + "http:", // Allow styles from HTTP sources (for development) ], - imgSrc: ["'self'", "data:", "https:"], - fontSrc: ["'self'", "data:", "https:"], + imgSrc: ["'self'", "data:", "https:", "http:"], + fontSrc: ["'self'", "data:", "https:", "http:"], connectSrc: ["'self'", "https:", "http:", "ws:", "wss:"], // Allow API calls frameSrc: ["'none'"], objectSrc: ["'none'"], // upgradeInsecureRequests removed - causes issues with reverse proxy + // Instead, we rely on the reverse proxy to handle HTTPS }, }, crossOriginEmbedderPolicy: false, // Disable for better compatibility + crossOriginResourcePolicy: { policy: "cross-origin" }, // Allow cross-origin resources }) ); @@ -175,14 +182,37 @@ if (fs.existsSync(frontendDistPath)) { } // Serve static files with proper headers for SPA - // Use middleware to set headers with access to request object + // Middleware to set CORS and security headers for all static assets app.use((req, res, next) => { - // Set CORS headers for assets if needed + // Get the origin from request (for CORS) const origin = req.headers.origin; + const protocol = req.protocol; // 'http' or 'https' (respects X-Forwarded-Proto) + const host = req.get('host'); + + // Set CORS headers for assets (allow same-origin and configured origins) if (origin) { - res.setHeader('Access-Control-Allow-Origin', origin); - res.setHeader('Access-Control-Allow-Credentials', 'true'); + // Check if origin is allowed + const allowedOrigins = getAllowedOrigins(); + if (isDevelopment || allowedOrigins.includes(origin) || origin.includes(host)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + } + } else { + // Same-origin request (no origin header), allow it + res.setHeader('Access-Control-Allow-Origin', '*'); } + + // Security headers for all static assets + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + + // Cache control for assets + if (req.path.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/)) { + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); + } + next(); }); @@ -190,15 +220,20 @@ if (fs.existsSync(frontendDistPath)) { maxAge: '1y', // Cache static assets etag: true, lastModified: true, - setHeaders: (res, filePath) => { + setHeaders: (res, filePath, stat) => { // Set proper content type for JS/CSS files if (filePath.endsWith('.js')) { res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); } else if (filePath.endsWith('.css')) { res.setHeader('Content-Type', 'text/css; charset=utf-8'); + } else if (filePath.endsWith('.html')) { + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + } + + // Ensure CORS headers are set (in case middleware didn't catch it) + if (!res.getHeader('Access-Control-Allow-Origin')) { + res.setHeader('Access-Control-Allow-Origin', '*'); } - // Security headers for assets - res.setHeader('X-Content-Type-Options', 'nosniff'); }, })); } else { @@ -262,16 +297,57 @@ if (fs.existsSync(frontendDistPath)) { logger.info(`SPA fallback: serving index.html for ${req.path}`); } + // Set proper headers for index.html + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + + // Set CORS headers for index.html if needed + const origin = req.headers.origin; + if (origin) { + const allowedOrigins = getAllowedOrigins(); + if (isDevelopment || allowedOrigins.includes(origin) || origin.includes(req.get('host'))) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + } + } + + // Prevent caching of index.html (assets are cached, but HTML should not be) + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + // Serve frontend SPA - res.sendFile(indexHtmlPath, (err) => { + // Read the file and potentially rewrite HTTP URLs to HTTPS if needed + fs.readFile(indexHtmlPath, 'utf8', (err, html) => { if (err) { - logger.error(`❌ Failed to send index.html: ${err.message}`); + logger.error(`❌ Failed to read index.html: ${err.message}`); logger.error(`Error stack: ${err.stack}`); - res.status(500).json({ + return res.status(500).json({ success: false, error: 'Failed to load frontend', }); } + + // If the request is HTTPS (via reverse proxy), rewrite HTTP asset URLs to HTTPS + // This fixes mixed content issues + const protocol = req.protocol; // Will be 'https' if X-Forwarded-Proto is set correctly + if (protocol === 'https') { + // Replace absolute HTTP URLs with HTTPS (but keep relative URLs as-is) + html = html.replace(/href="http:\/\/([^"]+)"/g, (match, url) => { + if (url.includes(req.get('host'))) { + return `href="https://${url}"`; + } + return match; + }); + html = html.replace(/src="http:\/\/([^"]+)"/g, (match, url) => { + if (url.includes(req.get('host'))) { + return `src="https://${url}"`; + } + return match; + }); + } + + res.send(html); }); }); } else {