Frontend Labs
⋯

Day 100 — HTTP Header Fingerprinting

Learning objective Score requests based on header consistency vs claimed User-Agent
Key packages net/http, strings
Security context Real browsers send specific headers in a consistent order; bots don't bother

Key Concepts

Chrome 120 always sends these headers (in this order):

:method, :authority, :scheme, :path  (HTTP/2 pseudo-headers)
sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform
Upgrade-Insecure-Requests
User-Agent
Accept
Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest
Referer (when navigating)
Accept-Encoding: gzip, deflate, br
Accept-Language

curl sends:

User-Agent, Accept, Host (only)

Exercise — Header Consistency Detector

package bot

import (
    "net/http"
    "strings"
)

// browserProfile defines expected headers for a claimed browser
type browserProfile struct {
    requiredHeaders  []string // must be present
    forbiddenValues  map[string]string // header → value that browser never sends
    minHeaderCount   int
}

var profiles = map[string]browserProfile{
    "Chrome": {
        requiredHeaders: []string{"Accept-Language", "Accept-Encoding"},
        forbiddenValues: map[string]string{
            "Accept-Encoding": "compress", // Chrome never sends compress
        },
        minHeaderCount: 6,
    },
    "Firefox": {
        requiredHeaders: []string{"Accept-Language", "Accept-Encoding"},
        forbiddenValues: map[string]string{},
        minHeaderCount: 5,
    },
    "Safari": {
        requiredHeaders: []string{"Accept-Language"},
        forbiddenValues: map[string]string{},
        minHeaderCount: 5,
    },
}

// claimedBrowser extracts the browser name from User-Agent
func claimedBrowser(ua string) string {
    ua = strings.ToLower(ua)
    switch {
    case strings.Contains(ua, "chrome") && !strings.Contains(ua, "edg"):
        return "Chrome"
    case strings.Contains(ua, "firefox"):
        return "Firefox"
    case strings.Contains(ua, "safari") && !strings.Contains(ua, "chrome"):
        return "Safari"
    case strings.Contains(ua, "curl"):
        return "curl"
    case strings.Contains(ua, "python"):
        return "python"
    case strings.Contains(ua, "go-http-client"):
        return "go-http"
    }
    return ""
}

// HeaderConsistencyDetector implements Detector
type HeaderConsistencyDetector struct{}

func (d *HeaderConsistencyDetector) Detect(r *http.Request, session *SessionScore) {
    ua := r.Header.Get("User-Agent")
    if ua == "" {
        session.AddSignal(Signal{"missing_ua", 20, CategoryHTTP, "No User-Agent header"})
        return
    }

    browser := claimedBrowser(ua)

    // Known non-browser clients — flag immediately
    switch browser {
    case "curl":
        session.AddSignal(Signal{"curl_ua", 25, CategoryHTTP, "curl User-Agent detected"})
        return
    case "python":
        session.AddSignal(Signal{"python_ua", 25, CategoryHTTP, "Python http client detected"})
        return
    case "go-http":
        session.AddSignal(Signal{"go_http_ua", 20, CategoryHTTP, "Go HTTP client detected"})
        return
    case "":
        session.AddSignal(Signal{"unknown_ua", 10, CategoryHTTP, "Unrecognized User-Agent"})
    }

    profile, ok := profiles[browser]
    if !ok {
        return
    }

    // Check required headers
    for _, h := range profile.requiredHeaders {
        if r.Header.Get(h) == "" {
            session.AddSignal(SigMissingAcceptLang)
        }
    }

    // Check forbidden values
    for header, forbidden := range profile.forbiddenValues {
        if strings.Contains(r.Header.Get(header), forbidden) {
            session.AddSignal(SigBadAcceptEncoding)
        }
    }

    // Check minimum header count
    if len(r.Header) < profile.minHeaderCount {
        session.AddSignal(Signal{
            "low_header_count", 15, CategoryHTTP,
            fmt.Sprintf("%s normally sends ≥%d headers, got %d", browser, profile.minHeaderCount, len(r.Header)),
        })
    }

    // Check Accept header consistency
    accept := r.Header.Get("Accept")
    if browser == "Chrome" && accept == "*/*" {
        // Chrome never sends Accept: */* for page navigation
        session.AddSignal(Signal{"bad_accept", 10, CategoryHTTP, "Chrome never sends Accept: */*"})
    }

    // User-Agent vs Sec-CH-UA consistency (HTTP/2 only)
    secCHUA := r.Header.Get("Sec-CH-UA")
    if browser == "Chrome" && r.ProtoMajor >= 2 && secCHUA == "" {
        session.AddSignal(SigUserAgentMismatch)
    }
}

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