Frontend Labs
⋯

Day 105 — Bot Mitigation Strategies

Learning objective Implement tiered bot mitigation: slow → challenge → block → honeypot → shadow-ban
Key packages net/http, time, sync, encoding/json
Security context Hard blocking causes false positives; tiered mitigation reduces impact on legitimate users while imposing maximum cost on bots

Key Concepts

Mitigation tiers:

Score Response Bot effect Legitimate user effect
20–40 +500ms delay Reduces throughput Barely noticeable
40–70 PoW challenge Requires 65k SHA256 ops Solved by browser JS in ~100ms
> 70 403 block Request rejected False positive risk
Honeypot Permanent flag IP permanently flagged Users never find hidden endpoint
Shadow-ban Fake 200 with empty data Bot thinks it's working Never applied to real users

Exercise — Tiered Bot Mitigation Middleware

package bot

import (
    "encoding/json"
    "net/http"
    "sync"
    "time"
)

// HoneypotTracker permanently flags any IP that hits a honeypot endpoint
type HoneypotTracker struct {
    mu      sync.RWMutex
    flagged map[string]time.Time // IP → when flagged
}

func NewHoneypotTracker() *HoneypotTracker {
    return &HoneypotTracker{flagged: make(map[string]time.Time)}
}

func (ht *HoneypotTracker) Flag(ip string) {
    ht.mu.Lock()
    ht.flagged[ip] = time.Now()
    ht.mu.Unlock()
}

func (ht *HoneypotTracker) IsFlagged(ip string) bool {
    ht.mu.RLock()
    _, ok := ht.flagged[ip]
    ht.mu.RUnlock()
    return ok
}

// HoneypotHandler — any IP that calls this gets permanently flagged
// The path should be hidden (never linked from real pages, never in sitemap)
func (ht *HoneypotTracker) HoneypotHandler(scorer *BotScorer) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := clientIP(r)
        ht.Flag(ip)

        // Add maximum score signal to BotScorer
        session := scorer.sessionFor(ip)
        session.AddSignal(SigHoneypotHit)

        // Return a plausible 200 — don't reveal that this is a honeypot
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]interface{}{
            "data":  []interface{}{},
            "total": 0,
            "page":  1,
        })
    })
}

// ShadowBanHandler returns plausible but empty data for confirmed bots
func ShadowBanHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Return 200 with empty/fake data
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]interface{}{
            "results": []interface{}{},
            "count":   0,
        })
        // Do NOT call next — backend never sees the request
    })
}

// TieredBotMiddleware applies mitigation based on bot score
type TieredBotMiddleware struct {
    scorer   *BotScorer
    pow      *PowSystem
    honeypot *HoneypotTracker

    // Thresholds
    delayScore    float64 // score >= this → add artificial delay
    challengeScore float64 // score >= this → PoW challenge
    blockScore    float64 // score >= this → 403
    shadowScore   float64 // score >= this (confirmed) → shadow ban

    delayAmount time.Duration
}

func NewTieredBotMiddleware(scorer *BotScorer, pow *PowSystem, honeypot *HoneypotTracker) *TieredBotMiddleware {
    return &TieredBotMiddleware{
        scorer:         scorer,
        pow:            pow,
        honeypot:       honeypot,
        delayScore:     20,
        challengeScore: 40,
        blockScore:     70,
        shadowScore:    85,
        delayAmount:    500 * time.Millisecond,
    }
}

func (m *TieredBotMiddleware) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := clientIP(r)

        // Honeypot flagged → shadow ban (permanent)
        if m.honeypot.IsFlagged(ip) {
            ShadowBanHandler(next).ServeHTTP(w, r)
            return
        }

        // Skip challenge if they have a valid PoW session
        if m.pow.HasValidSession(r) {
            next.ServeHTTP(w, r)
            return
        }

        score, _ := m.scorer.Score(r)

        switch {
        case score >= m.shadowScore:
            // Confirmed bot — shadow ban
            ShadowBanHandler(next).ServeHTTP(w, r)

        case score >= m.blockScore:
            // High confidence bot — hard block
            http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)

        case score >= m.challengeScore:
            // Medium confidence — serve PoW challenge
            w.Header().Set("X-Bot-Challenge", "required")
            http.Redirect(w, r, "/challenge", http.StatusFound)

        case score >= m.delayScore:
            // Low confidence — slow down
            time.Sleep(m.delayAmount)
            next.ServeHTTP(w, r)

        default:
            next.ServeHTTP(w, r)
        }
    })
}

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