97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import helmet from 'helmet';
|
|
import rateLimit from 'express-rate-limit';
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { config } from './config';
|
|
import { securityLogger } from './logger';
|
|
|
|
// Security headers middleware
|
|
export const securityHeaders = helmet({
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
scriptSrc: ["'self'"],
|
|
imgSrc: ["'self'", "data:", "blob:"],
|
|
connectSrc: ["'self'"],
|
|
fontSrc: ["'self'"],
|
|
objectSrc: ["'none'"],
|
|
mediaSrc: ["'self'"],
|
|
frameSrc: ["'none'"],
|
|
},
|
|
},
|
|
crossOriginEmbedderPolicy: false,
|
|
hsts: {
|
|
maxAge: 31536000,
|
|
includeSubDomains: true,
|
|
preload: true
|
|
}
|
|
});
|
|
|
|
// Rate limiting
|
|
export const createRateLimit = rateLimit({
|
|
windowMs: config.rateLimit.windowMs,
|
|
max: config.rateLimit.maxRequests,
|
|
message: {
|
|
error: 'Too many requests from this IP, please try again later.',
|
|
retryAfter: Math.ceil(config.rateLimit.windowMs / 1000)
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
keyGenerator: (req: Request) => {
|
|
const forwarded = req.headers['x-forwarded-for'];
|
|
if (forwarded) {
|
|
return typeof forwarded === 'string'
|
|
? forwarded.split(',')[0].trim()
|
|
: forwarded[0];
|
|
}
|
|
return req.ip ||
|
|
req.headers['x-real-ip'] as string ||
|
|
req.connection.remoteAddress ||
|
|
'unknown';
|
|
},
|
|
handler: (req: Request, res: Response) => {
|
|
securityLogger.rateLimitExceeded(
|
|
req.ip || req.connection.remoteAddress || 'unknown',
|
|
req.path
|
|
);
|
|
res.status(429).json({
|
|
error: 'Too many requests from this IP, please try again later.',
|
|
retryAfter: Math.ceil(config.rateLimit.windowMs / 1000)
|
|
});
|
|
}
|
|
});
|
|
|
|
// Request logging
|
|
export const requestLogger = (req: Request, res: Response, next: NextFunction) => {
|
|
next();
|
|
};
|
|
|
|
// Sanitize input
|
|
export const sanitizeInput = (req: Request, res: Response, next: NextFunction) => {
|
|
// Recursively sanitize string values
|
|
const sanitizeValue = (value: any): any => {
|
|
if (typeof value === 'string') {
|
|
return value
|
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') // Remove script tags
|
|
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
|
.trim();
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map(sanitizeValue);
|
|
}
|
|
if (value && typeof value === 'object') {
|
|
const sanitized: any = {};
|
|
for (const [key, val] of Object.entries(value)) {
|
|
sanitized[key] = sanitizeValue(val);
|
|
}
|
|
return sanitized;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
req.body = sanitizeValue(req.body);
|
|
req.query = sanitizeValue(req.query);
|
|
req.params = sanitizeValue(req.params);
|
|
|
|
next();
|
|
}; |