Frontend Labs
⋯

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.


Generated from week9.md · 8 Oct 2026 16:36