Описание
Gitea: ParseAcceptLanguage quadratic-time DoS via Locale middleware on unauthenticated requests
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 readingmodules/web/middleware/locale.go(the call site at line 38 is unchanged onmain)
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):
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:
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):
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.
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:
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.
Пакеты
code.gitea.io/gitea
< 1.27.0
1.27.0
Связанные уязвимости
ParseAcceptLanguage quadratic-time DoS via Locale middleware on unauthenticated requests
ParseAcceptLanguage quadratic-time DoS via Locale middleware on unauthenticated requests