import {Response} from '@loopback/rest'; import {DEFAULT_SECURITY_HEADERS_CONFIG} from '../constants/security-defaults'; import {SecurityHeadersConfig} from '../types'; import {SecurityLogger} from '../utils/security-logger'; /** * Security headers handler that applies security-related HTTP headers */ export class SecurityHeadersHandler { constructor( private config?: SecurityHeadersConfig, private logger?: SecurityLogger ) { // Set default configuration this.config = { ...DEFAULT_SECURITY_HEADERS_CONFIG, ...config }; this.logger?.logSecurityEvent({}, { timestamp: new Date().toISOString(), eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', metadata: { component: 'SecurityHeadersHandler', headersEnabled: this.getEnabledHeaders() } }); } /** * Apply security headers to response */ async applyHeaders(res: Response): Promise { // Content Security Policy if (this.config?.contentSecurityPolicy) { this.setContentSecurityPolicy(res); } // HTTP Strict Transport Security (HSTS) if (this.config?.hsts) { this.setHsts(res); } // X-Content-Type-Options if (this.config?.noSniff) { res.setHeader('X-Content-Type-Options', 'nosniff'); } // X-XSS-Protection if (this.config?.xssFilter) { res.setHeader('X-XSS-Protection', '1; mode=block'); } // Referrer Policy if (this.config?.referrerPolicy) { const policy = typeof this.config.referrerPolicy === 'string' ? this.config.referrerPolicy : 'strict-origin-when-cross-origin'; res.setHeader('Referrer-Policy', policy); } // X-Frame-Options if (this.config?.frameOptions) { const frameOption = typeof this.config.frameOptions === 'string' ? this.config.frameOptions : 'DENY'; res.setHeader('X-Frame-Options', frameOption); } // X-Permitted-Cross-Domain-Policies if (this.config?.permittedCrossDomainPolicies === false) { res.setHeader('X-Permitted-Cross-Domain-Policies', 'none'); } // Remove X-Powered-By header if (this.config?.hidePoweredBy) { res.removeHeader('X-Powered-By'); } // Additional security headers this.setAdditionalSecurityHeaders(res); this.logger?.logSecurityEvent({}, { timestamp: new Date().toISOString(), eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', metadata: { component: 'SecurityHeadersHandler', action: 'HEADERS_APPLIED', appliedHeaders: this.getAppliedHeaders(res) } }); } /** * Set Content Security Policy header */ private setContentSecurityPolicy(res: Response): void { const csp = this.config?.contentSecurityPolicy; if (typeof csp === 'string') { res.setHeader('Content-Security-Policy', csp); } else if (csp === true) { // More permissive default CSP policy for better compatibility const defaultPolicy = [ "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:", "script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:", "style-src 'self' 'unsafe-inline' data: https:", "img-src 'self' data: blob: https: http:", "font-src 'self' data: https:", "connect-src 'self' https: http: ws: wss:", "media-src 'self' data: blob: https:", "frame-src 'self' https:", "worker-src 'self' blob:", "child-src 'self' blob:", "base-uri 'self'", "object-src 'none'", "manifest-src 'self'" ].join('; '); res.setHeader('Content-Security-Policy', defaultPolicy); } } /** * Set HTTP Strict Transport Security header */ private setHsts(res: Response): void { const hstsConfig = this.config?.hsts; if (typeof hstsConfig === 'boolean' && hstsConfig) { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } else if (typeof hstsConfig === 'object') { let hstsValue = `max-age=${hstsConfig.maxAge ?? 31536000}`; if (hstsConfig.includeSubDomains) { hstsValue += '; includeSubDomains'; } if (hstsConfig.preload) { hstsValue += '; preload'; } res.setHeader('Strict-Transport-Security', hstsValue); } } /** * Set additional security headers */ private setAdditionalSecurityHeaders(res: Response): void { // X-DNS-Prefetch-Control res.setHeader('X-DNS-Prefetch-Control', 'off'); // X-Download-Options (IE8+) res.setHeader('X-Download-Options', 'noopen'); // Cache-Control for sensitive pages res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } /** * Get list of enabled headers */ private getEnabledHeaders(): string[] { const enabled: string[] = []; if (this.config?.contentSecurityPolicy) enabled.push('Content-Security-Policy'); if (this.config?.hsts) enabled.push('Strict-Transport-Security'); if (this.config?.noSniff) enabled.push('X-Content-Type-Options'); if (this.config?.xssFilter) enabled.push('X-XSS-Protection'); if (this.config?.referrerPolicy) enabled.push('Referrer-Policy'); if (this.config?.frameOptions) enabled.push('X-Frame-Options'); if (this.config?.permittedCrossDomainPolicies === false) enabled.push('X-Permitted-Cross-Domain-Policies'); return enabled; } /** * Get headers that were applied to response */ private getAppliedHeaders(res: Response): string[] { const headers: string[] = []; const responseHeaders = res.getHeaders(); const securityHeaders = [ 'Content-Security-Policy', 'Strict-Transport-Security', 'X-Content-Type-Options', 'X-XSS-Protection', 'Referrer-Policy', 'X-Frame-Options', 'X-Permitted-Cross-Domain-Policies', 'X-DNS-Prefetch-Control', 'X-Download-Options', 'Cache-Control' ]; for (const header of securityHeaders) { if (responseHeaders[header.toLowerCase()]) { headers.push(header); } } return headers; } /** * Update CSP policy */ updateContentSecurityPolicy(policy: string): void { if (!this.config) { this.config = {}; } this.config.contentSecurityPolicy = policy; this.logger?.logSecurityEvent({}, { timestamp: new Date().toISOString(), eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', metadata: { component: 'SecurityHeadersHandler', action: 'CSP_UPDATED', newPolicy: policy.substring(0, 100) + (policy.length > 100 ? '...' : '') } }); } /** * Update HSTS configuration */ updateHsts(maxAge: number, includeSubDomains = true, preload = false): void { if (!this.config) { this.config = {}; } this.config.hsts = {maxAge, includeSubDomains, preload}; this.logger?.logSecurityEvent({}, { timestamp: new Date().toISOString(), eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', metadata: { component: 'SecurityHeadersHandler', action: 'HSTS_UPDATED', maxAge, includeSubDomains, preload } }); } /** * Enable or disable specific header */ toggleHeader(headerName: keyof SecurityHeadersConfig, enabled: boolean): void { if (!this.config) { this.config = {}; } (this.config as Record)[headerName] = enabled; this.logger?.logSecurityEvent({}, { timestamp: new Date().toISOString(), eventType: 'SECURITY_CHECK_PASSED', severity: 'LOW', metadata: { component: 'SecurityHeadersHandler', action: 'HEADER_TOGGLED', headerName, enabled } }); } /** * Get current configuration */ getConfig(): SecurityHeadersConfig | undefined { return this.config; } /** * Get security headers analysis */ getHeadersAnalysis(): { totalHeaders: number; enabledHeaders: string[]; disabledHeaders: string[]; securityScore: number; } { const allHeaders = [ 'contentSecurityPolicy', 'hsts', 'noSniff', 'xssFilter', 'referrerPolicy', 'frameOptions', 'permittedCrossDomainPolicies', 'hidePoweredBy' ]; const enabledHeaders: string[] = []; const disabledHeaders: string[] = []; for (const header of allHeaders) { const value = (this.config as Record)?.[header]; if (value) { enabledHeaders.push(header); } else { disabledHeaders.push(header); } } const securityScore = Math.round((enabledHeaders.length / allHeaders.length) * 100); return { totalHeaders: allHeaders.length, enabledHeaders, disabledHeaders, securityScore }; } /** * Update configuration */ updateConfig(config: Partial): void { this.config = {...this.config, ...config}; } }