enhanced-middleware.ts•5.89 kB
import express, { Request, Response, NextFunction, Router } from "express";
import sanitizeHtml from "sanitize-html";
// Custom error type with status code
class SecurityError extends Error {
constructor(
public message: string,
public statusCode: number,
) {
super(message);
this.name = "SecurityError";
}
}
// Security configuration
const SECURITY_CONFIG = {
FRAME_OPTIONS: "DENY",
XSS_PROTECTION: "1; mode=block",
REFERRER_POLICY: "strict-origin-when-cross-origin",
HSTS_MAX_AGE: 31536000, // 1 year in seconds
CSP: {
"default-src": ["'self'"],
"script-src": ["'self'", "'unsafe-inline'"],
"style-src": ["'self'", "'unsafe-inline'"],
"img-src": ["'self'", "data:", "https:"],
"font-src": ["'self'"],
"connect-src": ["'self'"],
"frame-ancestors": ["'none'"],
"form-action": ["'self'"],
},
// Request validation config
MAX_URL_LENGTH: 2048,
MAX_BODY_SIZE: "50kb",
// Rate limiting config
RATE_LIMIT: {
windowMs: 15 * 60 * 1000,
max: 50,
},
AUTH_RATE_LIMIT: {
windowMs: 15 * 60 * 1000,
max: 3,
},
};
export class SecurityMiddleware {
private static app: express.Express;
private static requestCounts: Map<string, { count: number; resetTime: number }> = new Map();
private static authRequestCounts: Map<string, { count: number; resetTime: number }> = new Map();
static initialize(app: express.Express): void {
this.app = app;
// Body parser middleware with size limit
app.use(
express.json({
limit: SECURITY_CONFIG.MAX_BODY_SIZE,
}),
);
// Error handling middleware for body-parser errors
app.use(
(error: any, _req: express.Request, res: express.Response, next: express.NextFunction) => {
if (error) {
return res.status(413).json({
error: true,
message: "Request body too large",
});
}
next();
},
);
// Main security middleware
app.use((req: Request, res: Response, next: NextFunction) => {
try {
// Apply security headers
SecurityMiddleware.applySecurityHeaders(res);
// Check rate limits
SecurityMiddleware.checkRateLimit(req);
// Validate request
SecurityMiddleware.validateRequest(req);
// Sanitize input
if (req.body) {
req.body = SecurityMiddleware.sanitizeInput(req.body);
}
next();
} catch (error) {
if (error instanceof SecurityError) {
res.status(error.statusCode).json({
error: true,
message: error.message,
});
} else {
res.status(500).json({
error: true,
message: "Internal server error",
});
}
}
});
}
private static validateRequest(req: Request): void {
// Check URL length
if (req.originalUrl.length > SECURITY_CONFIG.MAX_URL_LENGTH) {
throw new SecurityError("URL too long", 413);
}
// Check content type for POST requests
if (req.method === "POST" && req.headers["content-type"] !== "application/json") {
throw new SecurityError("Content-Type must be application/json", 415);
}
}
private static sanitizeInput(input: unknown): unknown {
if (typeof input === "string") {
return sanitizeHtml(input, {
allowedTags: [],
allowedAttributes: {},
});
} else if (Array.isArray(input)) {
return input.map((item) => SecurityMiddleware.sanitizeInput(item));
} else if (input && typeof input === "object") {
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(input)) {
sanitized[key] = SecurityMiddleware.sanitizeInput(value);
}
return sanitized;
}
return input;
}
private static applySecurityHeaders(res: Response): void {
// Remove X-Powered-By header
res.removeHeader("X-Powered-By");
// Set security headers
res.setHeader("X-Frame-Options", SECURITY_CONFIG.FRAME_OPTIONS);
res.setHeader("X-XSS-Protection", SECURITY_CONFIG.XSS_PROTECTION);
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Referrer-Policy", SECURITY_CONFIG.REFERRER_POLICY);
res.setHeader(
"Strict-Transport-Security",
`max-age=${SECURITY_CONFIG.HSTS_MAX_AGE}; includeSubDomains; preload`,
);
res.setHeader("X-Permitted-Cross-Domain-Policies", "none");
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
res.setHeader("Origin-Agent-Cluster", "?1");
// Set Content-Security-Policy
const cspDirectives = Object.entries(SECURITY_CONFIG.CSP)
.map(([key, values]) => `${key} ${values.join(" ")}`)
.join("; ");
res.setHeader("Content-Security-Policy", cspDirectives);
}
private static checkRateLimit(req: Request): void {
const ip = req.ip || req.socket.remoteAddress || "unknown";
const now = Date.now();
const isAuth = req.path.startsWith("/auth");
const store = isAuth ? SecurityMiddleware.authRequestCounts : SecurityMiddleware.requestCounts;
const config = isAuth ? SECURITY_CONFIG.AUTH_RATE_LIMIT : SECURITY_CONFIG.RATE_LIMIT;
let record = store.get(ip);
if (!record || now > record.resetTime) {
record = { count: 1, resetTime: now + config.windowMs };
} else {
record.count++;
if (record.count > config.max) {
throw new SecurityError(
isAuth ? "Too many authentication requests" : "Too many requests",
429,
);
}
}
store.set(ip, record);
}
// For testing purposes
public static clearRateLimits(): void {
SecurityMiddleware.requestCounts.clear();
SecurityMiddleware.authRequestCounts.clear();
}
}