highJul 14, 2026

Ech0: Accept-Language `_` Separator Bypass Causes ~70x CPU Amplification DoS

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Ech0's i18n middleware passes the raw Accept-Language HTTP header directly into a Go language parser on every request, and a 1 MB header full of underscore separators bypasses an existing DoS guard…

Packagegithub.com/lin-snow/ech0
Ecosystemgo
Affected< 5.0.1

The problem

Ech0's i18n.Middleware reads the full Accept-Language header and forwards it, unsanitized, to goi18n.NewLocalizer, which calls golang.org/x/text/language.ParseAcceptLanguage on every single HTTP request, including unauthenticated endpoints.

The BCP 47 tag parser has O(N²) time complexity. CVE-2022-32149 added a guard that counts - characters and rejects inputs with more than 1000. That guard does not count _ even though the parser's internal scanner treats _ identically to - before parsing.

A 1 MB header containing only _-separated tokens has zero - characters, clears the guard, and drives the quadratic code path. Go's default MaxHeaderBytes is 1 MiB, so an attacker can send the maximum allowed input with no special access.

With the X-Locale: en header also set, Ech0 calls ParseAcceptLanguage a second time on the combined explicit-locale + Accept-Language value, pushing per-request CPU time to roughly 7.9 seconds on the same host.

Proof of concept

A working proof-of-concept for this issue in github.com/lin-snow/ech0, with the exact payload below.

bash
# Single request: ~1.5 s CPU per request (default path)
PAYLOAD="en$(python3 -c 'print("_abcdefghi" * 100000, end="")')"
curl -sS -o /dev/null \
  -w 'http=%{http_code} t=%{time_total}\n' \
  -H "Accept-Language: ${PAYLOAD}" \
  http://TARGET:6277/

# Amplified path: ~7.9 s CPU per request (add X-Locale to trigger second parse)
curl -sS -o /dev/null \
  -w 'http=%{http_code} t=%{time_total}\n' \
  -H "Accept-Language: ${PAYLOAD}" \
  -H "X-Locale: en" \
  http://TARGET:6277/

Each _abcdefghi token is 10 characters long, which exceeds the scanner's 8-character tag-length limit. The scanner calls an internal gobble routine that memmoves the entire remaining buffer past the invalid token. With N such tokens the total bytes moved is O(N²), producing the observed ~1.5 s wall time for a 1 MiB payload.

The CVE-2022-32149 fix in golang.org/x/text v0.3.8 only counts - characters in its complexity guard, completely missing _ even though _ is aliased to - before parsing begins.

The patch adds a sanitizeAcceptLanguage helper at the middleware boundary that counts both - and _ separators and returns an empty string when the combined total exceeds 32, a ceiling no real browser ever reaches. The same sanitizer must be applied to the HeaderLocale path and the user.go call sites that also forward user input into ResolveLocale.

Root cause: CWE-400 (Uncontrolled Resource Consumption) and CWE-772 (Missing Release of Resource), combined with an incomplete upstream guard that normalizes _ to - only after the complexity check.

The fix

Upgrade to github.com/lin-snow/Ech0 v5.0.1 or later, which adds a sanitizeAcceptLanguage function that drops any Accept-Language value containing more than 32 combined - and _ separators before the value reaches the parser. If you cannot upgrade immediately, add an equivalent filter at your reverse proxy or WAF to reject Accept-Language headers longer than a few hundred bytes.

Reported by tonghuaroot.

References: [1][2]

Related research