Frontend Labs
⋯

Day 68 — WAF Fundamentals & OWASP Top 10

Learning Objectives

  • Map each OWASP Top 10 category to a WAF rule
  • Build the WAF skeleton: Rule interface, scoring middleware
  • Understand detection mode vs blocking mode

OWASP Top 10 → WAF Rule Mapping

OWASP Category WAF Defense Your Day
A01 Broken Access Control Auth middleware, CORS Day 67, 75
A02 Cryptographic Failures TLS enforcement, HSTS Day 39, 67
A03 Injection (SQLi, XSS, Cmd) WAF rules Day 69-71 Days 69-71
A04 Insecure Design Schema validation Day 78
A05 Security Misconfiguration Security headers Day 67
A06 Vulnerable Components N/A (code audit) —
A07 Auth Failures Rate limiting, MFA Day 76, 80
A08 Data Integrity Failures HMAC signing, ECDSA Day 42, 44
A09 Logging Failures Structured audit log Day 18, 84
A10 SSRF URL allowlisting Day 71

Exercise: WAF Skeleton

package waf

import (
    "fmt"
    "net/http"
    "sync"
    "sync/atomic"
)

// Rule defines a WAF detection rule
type Rule interface {
    ID() string                           // unique identifier, e.g., "SQLI-001"
    Category() string                     // "SQLI", "XSS", "TRAVERSAL", "CMDINJ"
    Match(r *http.Request, body []byte) (matched bool, score int, detail string)
}

// MatchResult records one rule match for audit logging
type MatchResult struct {
    RuleID    string
    Category  string
    Score     int
    Detail    string
    Location  string // "query", "header", "body"
    Raw       string // matched value (truncated)
}

// WAFConfig is hot-reloadable at runtime
type WAFConfig struct {
    Mode           string         // "off", "detection", "blocking"
    BlockThreshold int            // block if total score >= this
    CategoryThresh map[string]int // per-category thresholds
    TrustedCIDRs   []string       // bypass WAF completely
}

// WAFMiddleware is the main WAF entry point
type WAFMiddleware struct {
    rules    []Rule
    config   atomic.Value // *WAFConfig
    auditor  AuditWriter

    // Metrics
    requestsChecked atomic.Int64
    requestsBlocked atomic.Int64
    ruleHits        sync.Map // ruleID → count
}

func NewWAFMiddleware(rules []Rule, cfg *WAFConfig, auditor AuditWriter) *WAFMiddleware {
    m := &WAFMiddleware{rules: rules, auditor: auditor}
    m.config.Store(cfg)
    return m
}

func (waf *WAFMiddleware) Reload(cfg *WAFConfig) {
    waf.config.Store(cfg)
    fmt.Println("[WAF] config reloaded")
}

func (waf *WAFMiddleware) Handler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        cfg := waf.config.Load().(*WAFConfig)

        // Check if this IP is in the trusted CIDR bypass list
        if waf.isTrusted(r, cfg) {
            next.ServeHTTP(w, r)
            return
        }

        // Read and buffer body for inspection (with size limit)
        body, err := readBody(r)
        if err != nil {
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusRequestEntityTooLarge)
            fmt.Fprintf(w, `{"error":"request body too large"}`)
            return
        }
        restoreBody(r, body) // restore for downstream handler

        // Run all rules, collect scores
        var results []MatchResult
        totalScore := 0
        categoryScores := make(map[string]int)

        for _, rule := range waf.rules {
            matched, score, detail := rule.Match(r, body)
            if matched {
                loc, raw := findMatchLocation(r, body, detail)
                result := MatchResult{
                    RuleID:   rule.ID(),
                    Category: rule.Category(),
                    Score:    score,
                    Detail:   detail,
                    Location: loc,
                    Raw:      truncate(raw, 100),
                }
                results = append(results, result)
                totalScore += score
                categoryScores[rule.Category()] += score

                // Track rule hit metrics
                waf.ruleHits.Store(rule.ID(), 0)
            }
        }

        waf.requestsChecked.Add(1)

        // Determine action
        action := "allow"
        if len(results) > 0 {
            if waf.shouldBlock(cfg, totalScore, categoryScores) {
                action = "block"
            } else {
                action = "detect"
            }
        }

        // Audit log every match (regardless of action)
        if len(results) > 0 {
            waf.auditor.Log(r, results, action, totalScore)
        }

        if action == "block" && cfg.Mode == "blocking" {
            waf.requestsBlocked.Add(1)
            requestID, _ := r.Context().Value(requestIDKey).(string)
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusForbidden)
            // SECURITY: never reveal which rule triggered — information disclosure
            fmt.Fprintf(w, `{"error":"forbidden","request_id":%q}`, requestID)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func (waf *WAFMiddleware) shouldBlock(cfg *WAFConfig, total int, catScores map[string]int) bool {
    if total >= cfg.BlockThreshold {
        return true
    }
    for cat, score := range catScores {
        if thresh, ok := cfg.CategoryThresh[cat]; ok && score >= thresh {
            return true
        }
    }
    return false
}

Real-World Context

AWS WAF, Cloudflare WAF, and ModSecurity all use this scoring model. detection mode (log but don't block) is how you tune a new WAF deployment — run for 2 weeks in detection, review false positives, then enable blocking. Requiring WAF mode to be in config means a SIGHUP can enable/disable blocking without a deployment.


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