package middleware // Reference: P14 Deliverable #3 — Security Headers Middleware // Applies comprehensive HTTP security headers to all responses. // Chatwoot uses rack-protection and secure_headers.rb; GoChat needs equivalent middleware. import ( "github.com/gin-gonic/gin" ) // --- Security Audit Findings --- // // 1. CRITICAL: No security headers set on any response. // Chatwoot sets X-Frame-Options, CSP, HSTS via rack-protection. // Without these, the app is vulnerable to clickjacking, MIME sniffing, // and XSS injection attacks. // // 2. HIGH: Server header discloses Go version — enables targeted attacks. // Must be removed or replaced with generic value. // // 3. HIGH: No HSTS header — browser won't enforce HTTPS. // Chatwoot forces SSL in production; HSTS ensures browsers remember. // // 4. MEDIUM: No Permissions-Policy — browser APIs (camera, mic, geolocation) // can be silently accessed by compromised scripts. // // 5. MEDIUM: X-XSS-Protection set to "1" can introduce vulnerabilities // in older browsers. Modern best practice is "0" (disable legacy filter, // rely on CSP instead). // SecurityHeadersConfig holds configuration for security headers middleware. type SecurityHeadersConfig struct { // HSTSMaxAge specifies the max-age directive for Strict-Transport-Security. // Default: 31536000 (1 year). Set to 0 to disable HSTS. HSTSMaxAge int // HSTSIncludeSubDomains adds includeSubDomains to HSTS header. HSTSIncludeSubDomains bool // HSTSPreload adds preload to HSTS header for HSTS preload list submission. HSTSPreload bool // CSPDirectives specifies Content-Security-Policy directives. // If empty, a secure default policy is applied. CSPDirectives string // ReferrerPolicy specifies the Referrer-Policy header value. // Default: "strict-origin-when-cross-origin". ReferrerPolicy string // PermissionsPolicy specifies the Permissions-Policy header value. // If empty, a restrictive default is applied. PermissionsPolicy string } // DefaultSecurityHeadersConfig returns secure defaults matching // Chatwoot's rack-protection configuration with modern best practices. func DefaultSecurityHeadersConfig() SecurityHeadersConfig { return SecurityHeadersConfig{ HSTSMaxAge: 31536000, // 1 year HSTSIncludeSubDomains: true, HSTSPreload: false, CSPDirectives: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'", ReferrerPolicy: "strict-origin-when-cross-origin", PermissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()", } } // SecurityHeaders applies security headers to every response. // Headers applied: // - Content-Security-Policy: prevents XSS, clickjacking, data injection // - X-Frame-Options: DENY — legacy clickjacking protection for older browsers // - X-Content-Type-Options: nosniff — prevents MIME type sniffing // - Strict-Transport-Security: enforces HTTPS in browsers // - X-XSS-Protection: 0 — disables legacy IE XSS filter (modern CSP is better) // - Referrer-Policy: controls referrer information sent to other sites // - Permissions-Policy: restricts browser API access // - Server header is removed to prevent fingerprinting func SecurityHeaders(cfg SecurityHeadersConfig) gin.HandlerFunc { return func(c *gin.Context) { // Content-Security-Policy — primary XSS defense if cfg.CSPDirectives != "" { c.Header("Content-Security-Policy", cfg.CSPDirectives) } // X-Frame-Options: DENY — prevents framing entirely (legacy browsers) c.Header("X-Frame-Options", "DENY") // X-Content-Type-Options: nosniff — prevents browsers from MIME sniffing c.Header("X-Content-Type-Options", "nosniff") // Strict-Transport-Security — tells browsers to only use HTTPS if cfg.HSTSMaxAge > 0 { hstsValue := "max-age=" + itoa(cfg.HSTSMaxAge) if cfg.HSTSIncludeSubDomains { hstsValue += "; includeSubDomains" } if cfg.HSTSPreload { hstsValue += "; preload" } c.Header("Strict-Transport-Security", hstsValue) } // X-XSS-Protection: 0 — disable legacy XSS filter (modern CSP is preferred) // Setting to "1" can introduce vulnerabilities in IE/Edge legacy mode. c.Header("X-XSS-Protection", "0") // Referrer-Policy — control how much referrer info is shared if cfg.ReferrerPolicy != "" { c.Header("Referrer-Policy", cfg.ReferrerPolicy) } // Permissions-Policy — restrict access to browser APIs if cfg.PermissionsPolicy != "" { c.Header("Permissions-Policy", cfg.PermissionsPolicy) } // Remove Server header to prevent server fingerprinting c.Header("Server", "") c.Next() } } // itoa converts an int to its decimal string representation. // Avoids importing strconv for a single use. func itoa(n int) string { if n == 0 { return "0" } var buf [20]byte i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } return string(buf[i:]) }