/** * Security Middleware * Implements security headers and protections per DoD/MilSpec standards * * Complies with: * - DISA STIG: Web Server Security * - NIST SP 800-53: SI-4 (Information System Monitoring) * - NIST SP 800-171: 3.13.1 (Cryptographic Protection in Transit) */ import { FastifyRequest, FastifyReply } from 'fastify' import { randomBytes } from 'crypto' /** * Add security headers to responses per DoD/MilSpec requirements * * Implements comprehensive security headers as required by: * - DISA STIG for Web Servers * - OWASP Secure Headers Project * - DoD Security Technical Implementation Guides */ export async function securityHeadersMiddleware( request: FastifyRequest, reply: FastifyReply ): Promise { // Prevent MIME type sniffing (DISA STIG requirement) reply.header('X-Content-Type-Options', 'nosniff') // Prevent clickjacking attacks (DISA STIG requirement) reply.header('X-Frame-Options', 'DENY') // Legacy XSS protection (deprecated but still recommended for older browsers) reply.header('X-XSS-Protection', '1; mode=block') // HTTP Strict Transport Security (HSTS) with preload // max-age: 1 year (31536000 seconds) // includeSubDomains: Apply to all subdomains // preload: Allow inclusion in HSTS preload lists const hstsMaxAge = 31536000 // 1 year reply.header('Strict-Transport-Security', `max-age=${hstsMaxAge}; includeSubDomains; preload`) // Content Security Policy (CSP) per STIG requirements // Strict CSP to prevent XSS and injection attacks // Generate nonce for inline scripts/styles to avoid unsafe-inline const nonce = randomBytes(16).toString('base64') // Store nonce in request for use in templates ;(request as any).cspNonce = nonce const csp = [ "default-src 'self'", `script-src 'self' 'nonce-${nonce}'`, // Use nonce instead of unsafe-inline `style-src 'self' 'nonce-${nonce}'`, // Use nonce instead of unsafe-inline "img-src 'self' data: https:", "font-src 'self' data:", "connect-src 'self'", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", "upgrade-insecure-requests", ].join('; ') reply.header('Content-Security-Policy', csp) // Referrer Policy - control referrer information leakage reply.header('Referrer-Policy', 'strict-origin-when-cross-origin') // Permissions Policy (formerly Feature Policy) - disable unnecessary features reply.header('Permissions-Policy', [ 'geolocation=()', 'microphone=()', 'camera=()', 'payment=()', 'usb=()', 'magnetometer=()', 'gyroscope=()', 'accelerometer=()', ].join(', ')) // X-Permitted-Cross-Domain-Policies - restrict cross-domain policies reply.header('X-Permitted-Cross-Domain-Policies', 'none') // Expect-CT - Certificate Transparency (deprecated but still used) // Note: This header is deprecated but may still be required for some compliance // reply.header('Expect-CT', 'max-age=86400, enforce') // Cross-Origin-Embedder-Policy - prevent cross-origin data leakage reply.header('Cross-Origin-Embedder-Policy', 'require-corp') // Cross-Origin-Opener-Policy - isolate browsing context reply.header('Cross-Origin-Opener-Policy', 'same-origin') // Cross-Origin-Resource-Policy - control resource loading reply.header('Cross-Origin-Resource-Policy', 'same-origin') // Remove server information disclosure // Note: Fastify doesn't expose server header by default, but we ensure it's not set reply.removeHeader('Server') reply.removeHeader('X-Powered-By') } /** * Input sanitization helper */ export function sanitizeInput(input: unknown): unknown { if (typeof input === 'string') { // Remove potentially dangerous characters return input .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/javascript:/gi, '') .replace(/on\w+\s*=/gi, '') .trim() } if (Array.isArray(input)) { return input.map(sanitizeInput) } if (input && typeof input === 'object' && !Array.isArray(input)) { const sanitized: Record = {} for (const key in input) { if (Object.prototype.hasOwnProperty.call(input, key)) { sanitized[key] = sanitizeInput((input as Record)[key]) } } return sanitized } return input } /** * Validate and sanitize request body */ export async function sanitizeBodyMiddleware( request: FastifyRequest, reply: FastifyReply ): Promise { if (request.body) { request.body = sanitizeInput(request.body) } }