156 lines
4.3 KiB
JavaScript
156 lines
4.3 KiB
JavaScript
const helmet = require('helmet');
|
|
|
|
/**
|
|
* Helmet security configuration
|
|
* Helmet helps secure Express apps by setting various HTTP headers
|
|
*/
|
|
const helmetConfig = helmet({
|
|
// Content Security Policy
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for Swagger UI
|
|
scriptSrc: ["'self'", "'unsafe-inline'"], // Allow inline scripts for Swagger UI
|
|
imgSrc: ["'self'", "data:", "https:"],
|
|
connectSrc: ["'self'"],
|
|
fontSrc: ["'self'", "data:"],
|
|
objectSrc: ["'none'"],
|
|
mediaSrc: ["'self'"],
|
|
frameSrc: ["'none'"]
|
|
}
|
|
},
|
|
|
|
// Cross-Origin-Embedder-Policy
|
|
crossOriginEmbedderPolicy: false, // Disabled for API compatibility
|
|
|
|
// Cross-Origin-Opener-Policy
|
|
crossOriginOpenerPolicy: { policy: "same-origin" },
|
|
|
|
// Cross-Origin-Resource-Policy
|
|
crossOriginResourcePolicy: { policy: "cross-origin" },
|
|
|
|
// DNS Prefetch Control
|
|
dnsPrefetchControl: { allow: false },
|
|
|
|
// Expect-CT (deprecated but included for older browsers)
|
|
expectCt: { maxAge: 86400 },
|
|
|
|
// Frameguard (prevent clickjacking)
|
|
frameguard: { action: "deny" },
|
|
|
|
// Hide Powered-By header
|
|
hidePoweredBy: true,
|
|
|
|
// HTTP Strict Transport Security
|
|
hsts: {
|
|
maxAge: 31536000, // 1 year
|
|
includeSubDomains: true,
|
|
preload: true
|
|
},
|
|
|
|
// IE No Open
|
|
ieNoOpen: true,
|
|
|
|
// No Sniff (prevent MIME type sniffing)
|
|
noSniff: true,
|
|
|
|
// Origin-Agent-Cluster
|
|
originAgentCluster: true,
|
|
|
|
// Permitted Cross-Domain Policies
|
|
permittedCrossDomainPolicies: { permittedPolicies: "none" },
|
|
|
|
// Referrer Policy
|
|
referrerPolicy: { policy: "no-referrer" }
|
|
});
|
|
|
|
/**
|
|
* Custom security headers middleware
|
|
* Only adds headers not already set by Helmet
|
|
*/
|
|
const customSecurityHeaders = (req, res, next) => {
|
|
// Add Permissions-Policy (not in Helmet)
|
|
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
|
|
|
// Prevent caching of sensitive data
|
|
if (req.path.includes('/api/auth') || req.path.includes('/api/admin') || req.path.includes('/api/users')) {
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
res.setHeader('Pragma', 'no-cache');
|
|
res.setHeader('Expires', '0');
|
|
res.setHeader('Surrogate-Control', 'no-store');
|
|
}
|
|
|
|
next();
|
|
};
|
|
|
|
/**
|
|
* CORS configuration
|
|
*/
|
|
const getCorsOptions = () => {
|
|
const allowedOrigins = process.env.ALLOWED_ORIGINS
|
|
? process.env.ALLOWED_ORIGINS.split(',')
|
|
: ['http://localhost:3000', 'http://localhost:4200', 'http://localhost:5173'];
|
|
|
|
return {
|
|
origin: (origin, callback) => {
|
|
// Allow requests with no origin (mobile apps, Postman, etc.)
|
|
if (!origin) return callback(null, true);
|
|
|
|
if (allowedOrigins.indexOf(origin) !== -1 || process.env.NODE_ENV === 'development') {
|
|
callback(null, true);
|
|
} else {
|
|
callback(new Error('Not allowed by CORS'));
|
|
}
|
|
},
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
allowedHeaders: ['Content-Type', 'Authorization', 'x-guest-token'],
|
|
exposedHeaders: ['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset'],
|
|
maxAge: 86400 // 24 hours
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Security middleware for API routes
|
|
*/
|
|
const secureApiRoutes = (req, res, next) => {
|
|
// Log security-sensitive operations
|
|
if (req.method !== 'GET' && req.path.includes('/api/admin')) {
|
|
const logger = require('../config/logger');
|
|
logger.logSecurityEvent(`Admin ${req.method} request`, req);
|
|
}
|
|
|
|
next();
|
|
};
|
|
|
|
/**
|
|
* Prevent parameter pollution
|
|
* This middleware should be used after body parser
|
|
*/
|
|
const preventParameterPollution = (req, res, next) => {
|
|
// Whitelist of parameters that can have multiple values
|
|
const whitelist = ['category', 'difficulty', 'tags', 'keywords'];
|
|
|
|
// Check for duplicate parameters
|
|
if (req.query) {
|
|
for (const param in req.query) {
|
|
if (Array.isArray(req.query[param]) && !whitelist.includes(param)) {
|
|
return res.status(400).json({
|
|
status: 'error',
|
|
message: `Parameter pollution detected: '${param}' should not have multiple values`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
next();
|
|
};
|
|
|
|
module.exports = {
|
|
helmetConfig,
|
|
customSecurityHeaders,
|
|
getCorsOptions,
|
|
secureApiRoutes,
|
|
preventParameterPollution
|
|
};
|