Day 68 — WAF Fundamentals & OWASP Top 10
Learning Objectives
- Map each OWASP Top 10 category to a WAF rule
- Build the WAF skeleton:
Ruleinterface, 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.