Files
gochat/backend/internal/middleware/security_headers.go
T
2026-08-23 16:52:20 +08:00

140 lines
5.0 KiB
Go

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: blob:; 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:])
}