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)
}
})
}