Frontend Labs
⋯

Day 81 — CORS & Preflight

Learning Objectives

  • Implement strict origin allowlist CORS (never wildcard for authenticated APIs)
  • Handle OPTIONS preflight correctly with Vary: Origin
  • Test CORS configuration with multiple allowed and disallowed origins

Key Concepts

Same-Origin Policy:
  Browser blocks JS from reading cross-origin responses by default
  Without CORS: attacker.com JS cannot read your API response even with credentials

CORS Mistakes (OWASP A05):
  Access-Control-Allow-Origin: *   with credentials → CRITICAL
    Any site can make authenticated API calls using victim's cookies
  Reflect Origin without allowlist validation:
    if origin != "" { w.Header().Set("ACAO", origin) }  → VULNERABLE
  Vary: Origin missing → CDN caches wrong origin, serves to all

Correct CORS for authenticated APIs:
  Explicit allowlist (no wildcards)
  Vary: Origin (required for CDN correctness)
  Access-Control-Allow-Credentials: true only for specific trusted origins

Exercise: Strict CORS Middleware

package cors

import (
    "fmt"
    "net/http"
    "strings"
)

type Config struct {
    AllowedOrigins   []string // explicit list, no wildcards
    AllowedMethods   []string
    AllowedHeaders   []string
    ExposedHeaders   []string
    AllowCredentials bool
    MaxAge           int
}

type CORS struct {
    cfg            Config
    allowedOrigins map[string]bool
}

func New(cfg Config) *CORS {
    c := &CORS{
        cfg:            cfg,
        allowedOrigins: make(map[string]bool),
    }
    for _, o := range cfg.AllowedOrigins {
        c.allowedOrigins[o] = true
    }
    return c
}

func (c *CORS) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        origin := r.Header.Get("Origin")

        // Vary: Origin must ALWAYS be set when CORS headers might be present
        // Without it, CDN caches response for one origin and serves to all others
        w.Header().Add("Vary", "Origin")

        if origin == "" {
            // Not a CORS request — serve normally
            next.ServeHTTP(w, r)
            return
        }

        // Validate origin against allowlist — never reflect without validation
        if !c.allowedOrigins[origin] {
            // Don't set ACAO header — browser will block the response
            fmt.Printf("[CORS] blocked origin: %q\n", origin)
            next.ServeHTTP(w, r)
            return
        }

        // Set CORS headers for allowed origin
        w.Header().Set("Access-Control-Allow-Origin", origin) // never wildcard
        if c.cfg.AllowCredentials {
            w.Header().Set("Access-Control-Allow-Credentials", "true")
        }
        if len(c.cfg.ExposedHeaders) > 0 {
            w.Header().Set("Access-Control-Expose-Headers", strings.Join(c.cfg.ExposedHeaders, ", "))
        }

        // Handle preflight OPTIONS
        if r.Method == http.MethodOptions {
            w.Header().Set("Access-Control-Allow-Methods", strings.Join(c.cfg.AllowedMethods, ", "))
            w.Header().Set("Access-Control-Allow-Headers", strings.Join(c.cfg.AllowedHeaders, ", "))
            if c.cfg.MaxAge > 0 {
                w.Header().Set("Access-Control-Max-Age", fmt.Sprintf("%d", c.cfg.MaxAge))
            }
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func main() {
    cors := New(Config{
        AllowedOrigins:   []string{"https://app.example.com", "https://admin.example.com"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
        AllowedHeaders:   []string{"Content-Type", "Authorization"},
        ExposedHeaders:   []string{"X-Request-ID"},
        AllowCredentials: true,
        MaxAge:           3600,
    })

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, `{"ok":true}`)
    })

    fmt.Println("Testing CORS:")
    fmt.Println("  Allowed: curl -H 'Origin: https://app.example.com' http://localhost:8080/")
    fmt.Println("  Blocked: curl -H 'Origin: https://evil.com' http://localhost:8080/")

    http.ListenAndServe(":8080", cors.Middleware(handler))
}

Test:

# Should include ACAO header (allowed):
curl -s -I -H "Origin: https://app.example.com" http://localhost:8080/
# Access-Control-Allow-Origin: https://app.example.com

# Should NOT include ACAO header (blocked):
curl -s -I -H "Origin: https://evil.com" http://localhost:8080/
# (no Access-Control-Allow-Origin header)

Real-World Context

The CORS wildcard vulnerability (Access-Control-Allow-Origin: * with credentials) allows any website to make authenticated API calls using the victim's session. In 2018, this class of misconfiguration affected dozens of major platforms. The fix is 3 lines: use an allowlist, never reflect without validation, always set Vary: Origin.


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