217 lines
5.6 KiB
TypeScript
217 lines
5.6 KiB
TypeScript
/**
|
|
* Security Configuration
|
|
*
|
|
* Implements OWASP Top 10 security best practices
|
|
*/
|
|
|
|
import { HelmetOptions } from 'helmet';
|
|
|
|
export const helmetConfig: HelmetOptions = {
|
|
// Content Security Policy
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
styleSrc: ["'self'", "'unsafe-inline'"], // Required for inline styles in some frameworks
|
|
scriptSrc: ["'self'"],
|
|
imgSrc: ["'self'", 'data:', 'https:'],
|
|
connectSrc: ["'self'"],
|
|
fontSrc: ["'self'"],
|
|
objectSrc: ["'none'"],
|
|
mediaSrc: ["'self'"],
|
|
frameSrc: ["'none'"],
|
|
},
|
|
},
|
|
|
|
// Cross-Origin Embedder Policy
|
|
crossOriginEmbedderPolicy: false, // Set to true in production if needed
|
|
|
|
// Cross-Origin Opener Policy
|
|
crossOriginOpenerPolicy: { policy: 'same-origin' },
|
|
|
|
// Cross-Origin Resource Policy
|
|
crossOriginResourcePolicy: { policy: 'same-origin' },
|
|
|
|
// DNS Prefetch Control
|
|
dnsPrefetchControl: { allow: false },
|
|
|
|
// Frameguard
|
|
frameguard: { action: 'deny' },
|
|
|
|
// Hide Powered-By
|
|
hidePoweredBy: true,
|
|
|
|
// HSTS (HTTP Strict Transport Security)
|
|
hsts: {
|
|
maxAge: 31536000, // 1 year
|
|
includeSubDomains: true,
|
|
preload: true,
|
|
},
|
|
|
|
// IE No Open
|
|
ieNoOpen: true,
|
|
|
|
// No Sniff
|
|
noSniff: true,
|
|
|
|
// Origin Agent Cluster
|
|
originAgentCluster: true,
|
|
|
|
// Permitted Cross-Domain Policies
|
|
permittedCrossDomainPolicies: { permittedPolicies: 'none' },
|
|
|
|
// Referrer Policy
|
|
referrerPolicy: { policy: 'no-referrer' },
|
|
|
|
// XSS Filter
|
|
xssFilter: true,
|
|
};
|
|
|
|
/**
|
|
* Rate Limiting Configuration
|
|
*/
|
|
export const rateLimitConfig = {
|
|
// Global rate limit
|
|
global: {
|
|
ttl: 60, // 60 seconds
|
|
limit: 100, // 100 requests per minute
|
|
},
|
|
|
|
// Auth endpoints (more strict)
|
|
auth: {
|
|
ttl: 60,
|
|
limit: 5, // 5 login attempts per minute
|
|
},
|
|
|
|
// Search endpoints
|
|
search: {
|
|
ttl: 60,
|
|
limit: 30, // 30 searches per minute
|
|
},
|
|
|
|
// Booking endpoints
|
|
booking: {
|
|
ttl: 60,
|
|
limit: 20, // 20 bookings per minute
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Allowed CORS origins.
|
|
*
|
|
* Reads CORS_ORIGIN (comma-separated list — supports several frontend hosts such
|
|
* as app.* and www.*), falling back to FRONTEND_URL, then to localhost in dev.
|
|
* With `credentials: true` the browser requires an exact origin echo (no `*`),
|
|
* so we resolve the request origin against this allow-list per request.
|
|
*/
|
|
const allowedOrigins: string[] = (
|
|
process.env.CORS_ORIGIN ||
|
|
process.env.FRONTEND_URL ||
|
|
'http://localhost:3000,http://localhost:3001'
|
|
)
|
|
.split(',')
|
|
.map(origin => origin.trim())
|
|
.filter(Boolean);
|
|
|
|
/**
|
|
* CORS Configuration
|
|
*/
|
|
export const corsConfig = {
|
|
origin: (
|
|
requestOrigin: string | undefined,
|
|
callback: (err: Error | null, allow?: boolean) => void
|
|
): void => {
|
|
// Allow non-browser clients (no Origin header: server-to-server, curl, health checks).
|
|
if (!requestOrigin || allowedOrigins.includes(requestOrigin)) {
|
|
callback(null, true);
|
|
return;
|
|
}
|
|
callback(null, false);
|
|
},
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-CSRF-Token'],
|
|
exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
|
|
maxAge: 86400, // 24 hours
|
|
};
|
|
|
|
/**
|
|
* Password Policy
|
|
*/
|
|
export const passwordPolicy = {
|
|
minLength: 12,
|
|
requireUppercase: true,
|
|
requireLowercase: true,
|
|
requireNumbers: true,
|
|
requireSymbols: true,
|
|
maxLength: 128,
|
|
preventCommon: true, // Prevent common passwords
|
|
preventReuse: 5, // Last 5 passwords
|
|
};
|
|
|
|
/**
|
|
* File Upload Configuration
|
|
*/
|
|
export const fileUploadConfig = {
|
|
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
allowedMimeTypes: [
|
|
'application/pdf',
|
|
'image/jpeg',
|
|
'image/png',
|
|
'image/webp',
|
|
'application/vnd.ms-excel',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'text/csv',
|
|
],
|
|
allowedExtensions: ['.pdf', '.jpg', '.jpeg', '.png', '.webp', '.xls', '.xlsx', '.csv'],
|
|
scanForViruses: process.env.NODE_ENV === 'production',
|
|
};
|
|
|
|
/**
|
|
* Auth Cookie Configuration
|
|
*
|
|
* Tokens are delivered as httpOnly cookies so they are not readable from
|
|
* JavaScript (XSS mitigation). COOKIE_DOMAIN must be set in production when
|
|
* the API and the frontend live on different subdomains (e.g. ".xpeditis.com").
|
|
*/
|
|
export const AUTH_COOKIE_NAMES = {
|
|
accessToken: 'accessToken',
|
|
refreshToken: 'refreshToken',
|
|
/** Non-httpOnly flag the frontend reads to know a session exists (contains no token) */
|
|
session: 'xpeditis_session',
|
|
} as const;
|
|
|
|
export function authCookieOptions(options?: { maxAgeMs?: number; httpOnly?: boolean }): {
|
|
httpOnly: boolean;
|
|
secure: boolean;
|
|
sameSite: 'lax' | 'strict' | 'none';
|
|
path: string;
|
|
domain?: string;
|
|
maxAge?: number;
|
|
} {
|
|
// SameSite must be 'none' when the frontend and the API live on different
|
|
// sites (cross-origin), otherwise the browser drops the auth cookies set in
|
|
// the cross-site login XHR response. 'none' REQUIRES Secure (HTTPS).
|
|
// Configurable via COOKIE_SAMESITE; defaults to 'lax' for same-site setups.
|
|
const sameSite = (process.env.COOKIE_SAMESITE?.toLowerCase() as 'lax' | 'strict' | 'none') || 'lax';
|
|
const secure = process.env.NODE_ENV === 'production' || sameSite === 'none';
|
|
|
|
return {
|
|
httpOnly: options?.httpOnly ?? true,
|
|
secure,
|
|
sameSite,
|
|
path: '/',
|
|
...(process.env.COOKIE_DOMAIN ? { domain: process.env.COOKIE_DOMAIN } : {}),
|
|
...(options?.maxAgeMs ? { maxAge: options.maxAgeMs } : {}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Brute Force Protection
|
|
*/
|
|
export const bruteForceConfig = {
|
|
freeRetries: 3,
|
|
minWait: 5 * 60 * 1000, // 5 minutes
|
|
maxWait: 60 * 60 * 1000, // 1 hour
|
|
lifetime: 24 * 60 * 60, // 24 hours
|
|
};
|