140 lines
5.0 KiB
Go
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:])
|
|
}
|