Files
InviCanvas/server/middleware.ts
2026-01-16 17:06:43 +01:00

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();
};