This commit is contained in:
salvacybersec
2025-11-11 07:12:06 +03:00
parent 9a3f299c0c
commit fa7c4fbec0

View File

@@ -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 {