Day 67 — HTTP Security Headers
Learning Objectives
- Implement a complete security headers middleware
- Build allowlist-based CORS (never wildcard for credentialed APIs)
- Generate per-request CSP nonces for inline script whitelisting
Exercise: Complete Security Headers Middleware
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"strings"
)
type CORSConfig struct {
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
AllowCredentials bool
MaxAge int
}
func generateNonce() string {
b := make([]byte, 16)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
// SecurityHeadersMiddleware sets all required security headers
func SecurityHeadersMiddleware(cors CORSConfig) func(http.Handler) http.Handler {
allowedOriginSet := make(map[string]bool)
for _, o := range cors.AllowedOrigins {
allowedOriginSet[o] = true
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
// Generate CSP nonce for this request (for inline scripts)
nonce := generateNonce()
// Content Security Policy
h.Set("Content-Security-Policy",
fmt.Sprintf("default-src 'self'; script-src 'self' 'nonce-%s'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'", nonce))
// Inject nonce into context for templates to use
ctx := context.WithValue(r.Context(), cspNonceKey, nonce)
// HSTS (only over HTTPS)
if r.TLS != nil {
h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
}
// Anti-clickjacking
h.Set("X-Frame-Options", "DENY")
// MIME sniffing prevention
h.Set("X-Content-Type-Options", "nosniff")
// Referrer policy
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Permissions policy (disable unused browser APIs)
h.Set("Permissions-Policy", "accelerometer=(), camera=(), geolocation=(), microphone=(), payment=()")
// CORS handling
origin := r.Header.Get("Origin")
if origin != "" {
if allowedOriginSet[origin] {
h.Set("Access-Control-Allow-Origin", origin)
h.Set("Vary", "Origin") // critical: cache must vary by Origin
if cors.AllowCredentials {
h.Set("Access-Control-Allow-Credentials", "true")
}
} else {
// Don't set CORS headers for disallowed origins
// Browser will block the request
}
// Preflight
if r.Method == http.MethodOptions {
if allowedOriginSet[origin] {
h.Set("Access-Control-Allow-Methods", strings.Join(cors.AllowedMethods, ", "))
h.Set("Access-Control-Allow-Headers", strings.Join(cors.AllowedHeaders, ", "))
h.Set("Access-Control-Max-Age", fmt.Sprintf("%d", cors.MaxAge))
}
w.WriteHeader(http.StatusNoContent)
return
}
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func main() {
corsConfig := CORSConfig{
AllowedOrigins: []string{"https://app.example.com", "https://admin.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Request-ID"},
AllowCredentials: true,
MaxAge: 3600,
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nonce := r.Context().Value(cspNonceKey).(string)
fmt.Fprintf(w, `<html><body><script nonce="%s">console.log("CSP nonce works")</script></body></html>`, nonce)
})
srv := &http.Server{
Addr: ":8080",
Handler: SecurityHeadersMiddleware(corsConfig)(handler),
ReadHeaderTimeout: 5 * time.Second,
}
fmt.Println("Security headers server on :8080")
fmt.Println("Check headers: curl -I http://localhost:8080/")
fmt.Println("Test CORS: curl -H 'Origin: https://evil.com' http://localhost:8080/")
srv.ListenAndServe()
}
Real-World Context
Access-Control-Allow-Origin: * on an authenticated API endpoint is OWASP A05 (critical). It lets any website make authenticated API requests using the victim's cookies/tokens. The Vary: Origin header is required to prevent CDN caching a response with the wrong origin header. CSP nonces prevent XSS in inline scripts.