# Traefik entrypoint header-name sanitization bypassed via request trailers

Traefik's entrypoint header sanitization scans req.Header but not req.Trailer, letting clients smuggle aliased or X-Forwarded-* names as HTTP trailers past reject/delete and reach trailer-merging backends.

- Payout: Research
- Severity: high
- Program: Traefik
- Found by: bipol4r
- Disclosed: 2026-09-10
- CVE: CVE-2026-88004
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-v67p-phpq-fc8x
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-v67p-phpq-fc8x
- Tags: area:cloud domain:infra domain:web impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:go technique:parser-differential technique:source-review vuln:access-control vuln:auth-bypass vuln:request-smuggling

**Target:** Traefik v3 entrypoint header sanitization (aliasHeadersStrategy/underscoreHeadersStrategy, forwardedHeaders) and reverse proxy
**Root cause:** The four entrypoint sanitization handlers and forwardedheaders.DeleteXForwardedHeaders iterate only req.Header, never req.Trailer, despite doc comments promising to cover both; Go's server pre-fills declared trailer keys and the v3 custom reverse proxy forwards them to the backend.
**Steps:**
1. Configure entrypoint with `aliasHeadersStrategy = "reject"` (or `delete`) and a router to an h2c echo backend that prints r.Trailer.
2. Send a chunked HTTP/1.1 request declaring `Trailer: X_Auth_User` with body `5\r\nhello\r\n0\r\nX_Auth_User: attacker-value\r\n\r\n`.
3. Observe header form `X_Auth_User: x` returns 400, but trailer form returns 200 and backend logs `TRAILERS: map[X_auth_user:[]]`.
4. Repeat with `X.Auth.User` and `X-Forwarded-Prefix` trailers — all pass; delete mode also leaves the trailer name intact.
5. Add retry middleware with `status = ["500-599"]` (or buffering) so the body is read before Request.Clone; trailer values then reach the backend.
6. Point at a trailer-merging backend (pre-fix libevent CVE-2026-63379): header `X-Forwarded-Prefix: admin` → 403, trailer form → 200 ADMIN.
**How found:** Noticed the handler doc comments promise to cover 'header and trailer' while the code loops only over req.Header; traced Go's net/http trailer pre-fill (fixTrailer, httpcommon Setup Trailers) and Traefik's custom reverse proxy in pkg/proxy/httputil to confirm trailers are forwarded, then measured value survival with retry/buffering middleware and against real trailer-merging backends.
**Exploitation:** Unauthenticated client sends the trusted/aliased name as an HTTP/1.1 chunked or HTTP/2 trailer. On bare proxy paths only the name transits (empty value); with a body-buffering middleware (retry with status codes, or buffering) the attacker-chosen value survives the proxy clone and reaches a trailer-merging backend, flipping presence-based or value-based authorization (403→200 admin) and spoofing X-Forwarded-For IP allowlists.
**Escalation:** Chains Traefik's value-preserving buffering middleware with downstream trailer-merging components (libevent CVE-2026-63379, blaze CVE-2026-73495) to convert a name-level bypass into full value-level identity spoofing; bait declaration (`Trailer: X-Dummy`) enables undeclared trailer fields on HTTP/1.1 buffered chains.
**Insight:** When a proxy's sanitizer iterates req.Header, check whether the same names can arrive via req.Trailer — Go pre-fills declared trailer keys before handlers run, and body-buffering middleware (retry/buffering) reverses the read order so trailer values populate before Request.Clone, defeating the 'trailers arrive after security decisions' invariant.

## Proof of concept (copied exactly from the original report)

**Chunked request smuggling X_Auth_User as trailer**

```
POST / HTTP/1.1
Host: 127.0.0.1:8090
Connection: close
Transfer-Encoding: chunked
Trailer: X_Auth_User

5
hello
0
X_Auth_User: attacker-value
```

**Retry middleware config for value-preserving trailer path**

```
[http.routers.lib]
  entryPoints = ["web"]
  rule = "PathPrefix(`/`)"
  service = "lib"
  middlewares = ["retry-lib"]

[http.middlewares.retry-lib.retry]
  attempts = 2
  status = ["500-599"]

[http.services.lib.loadBalancer.servers]
  [http.services.lib.loadBalancer.servers.s1]
    url = "http://127.0.0.1:8083"
```

**Entrypoint aliasHeadersStrategy reject config**

```
[entryPoints.web]
  address = ":8090"
  [entryPoints.web.http]
    aliasHeadersStrategy = "reject"

[providers.file]
  filename = "dynamic.toml"
  watch = true
```

