GHSA-fw57-jgch-pgf3 Gitea: ParseAcceptLanguage quadratic-time DoS via Locale middleware on unauthenticated requests
SummaryThe 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.6` docker image (E2E below)`main` at commit `6f4027a6be28c876c0abaf37cc939658645b78a3` by reading
`modules/web/middleware/locale.go` (the call site at line 38 is unchanged
on `main`)Privilege requiredUnauthenticated. 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`](https://github.com/go-gitea/gitea/blob/fc396f0808187c358b4fc15dcefcd6957140a780/modules/web/middleware/locale.go#L38)
(blob SHA `fc396f0808187c358b4fc15dcefcd6957140a780`):`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`](https://github.com/golang/text/blob/v0.28.0/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 omits `lang`).`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 conceptSingle-line bash reproducer that crafts the malicious header and
times one request against a fresh `gitea/gitea:1.22.6` container: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.memmove`s 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):Captured run output (Apple M1 Pro, darwin/arm64, Go 1.26.1, the
official `gitea/gitea:1.22.6` image with no other tuning):Interpretation:| 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.ImpactOne 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 fixApply 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: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.CreditReported by tonghuaroot.
CVSS v4.0 score: 8.7