GitHub Advisoryadvisory · Go
GHSA-v67p-phpq-fc8x Traefik entrypoint header-name sanitization bypassed via request trailers
HIGH
Official page ↗
- Aliases
- CVE-2026-88004
- Ecosystem
- Go
- Packages
- github.com/traefik/traefik/v3
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N
- Weaknesses
- CWE-436, CWE-807
- Reviewed
- 2026-09-10
Affected packages
• Go github.com/traefik/traefik/v3: >= 3.2.0, < 3.7.13 → fixed in 3.7.13
Description
Summary
Traefik's entrypoint defenses against spoofed trusted header names — `aliasHeadersStrategy` / `underscoreHeadersStrategy` in `delete` or `reject` mode, and the default `forwardedHeaders` stripping of client-supplied `X-Forwarded-*` — scan `req.Header` only and never `req.Trailer`. An unauthenticated client can therefore smuggle a sanitized name (an aliasing spelling such as `X_Auth_User`, or a trusted name such as `X-Forwarded-Prefix`) as an HTTP/1.1 chunked trailer or an HTTP/2 trailer: `reject` does not return its documented `400`, `delete` does not remove the name, and Traefik's reverse proxy forwarded the trailer to the backend — with an attacker-chosen value whenever a body-buffering middleware (the `retry` middleware with status codes, or the `buffering` middleware) reads the body before the proxy clone. Backends that merge trailers into their header namespace then act on the smuggled name. The fix stops forwarding request trailer values to the backend; the declared trailer names are still forwarded as permitted by RFC 9110 section 6.6.2.
Traefik v2 is not affected: the defect is in the custom reverse proxy introduced in v3 (`pkg/proxy/httputil`), and v2 uses the Go standard library's `httputil.ReverseProxy`, which does not forward request trailer values to the backend. Affected v3 lines from v3.2.0 through v3.7.12 include the end-of-life v3.2 through v3.6 lines, which will not receive a fix on their own line; the remedy for those users is to upgrade to v3.7.13.
Patches
• https://github.com/traefik/traefik/releases/tag/v3.7.13
For more information
If you have any questions or comments about this advisory, please open an issue.
Original Description
Summary
Traefik's entrypoint defenses against spoofed header names — `aliasHeadersStrategy` / `underscoreHeadersStrategy` in `delete` or `reject` mode, and the `forwardedHeaders` handling that strips client-supplied `X-Forwarded-*` — scan `req.Header` only and never `req.Trailer`, although the handlers' own comments promise to cover "header and trailer". An unauthenticated client can therefore deliver the aliasing name (`X_Auth_User`, `X.Auth.User`) or the trusted name itself (`X-Forwarded-Prefix`, …) as an HTTP/1.1 chunked trailer or an HTTP/2 trailer: `reject` does not return its documented `400`, `delete` does not remove the name, and the trailer form of an `X-Forwarded-*` name passes exactly where the header form is stripped. When a body-buffering middleware is in the chain (retry with `status` codes, or the `buffering` middleware — both measured), the trailer travels with an attacker-chosen value; measured end-to-end against the trailer-merging component Ubuntu 24.04 ships (pre-fix libevent, CVE-2026-63379), the header `X-Forwarded-Prefix: admin` is stripped and denied while the identical name as a trailer is acted upon as admin (`403 → 200`). On bare proxy paths only the trailer name travels (no value), bounding those deployments to name-level effects.
Details
Root cause. All four entrypoint handlers iterate `req.Header` only — the doc comments promise more than the code does (`pkg/server/server_entrypoint_tcp.go`):
```go
// removeAliasingHeaders removes any request header and trailer whose name contains a character
// which is neither a letter, a digit, nor a dash, as such a name aliases another header name.
func removeAliasingHeaders(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
for key := range req.Header { // ← req.Trailer is never scanned
if isAliasingHeaderName(key) {
delete(req.Header, key)
}
}
h.ServeHTTP(rw, req)
})
}
```
`rejectAliasingHeaders`, `removeHeadersWithUnderscores` and `rejectHeadersWithUnderscores` share the identical structure (the `reject` variants return `400` from the same loop). The sibling sanitization `forwardedheaders.DeleteXForwardedHeaders` (`pkg/middlewares/forwardedheaders/forwarded_header.go`) also scans `req.Header` only, so the trusted `X-Forwarded-*` names whose header form Traefik strips for untrusted clients — the managed `XHeadersSet`, which includes `X-Forwarded-Prefix` and `X-Forwarded-For` — survive in trailer form. Go's HTTP server populates `req.Trailer` from chunked/HTTP/2 trailers, and Traefik's proxy layer forwards those entries, bypassing the sanitization above.
Contract provenance. The "header and trailer" wording is in the original introducing diffs — `108a52644` (underscoreHeadersStrategy) and `0331801c` (aliasHeadersStrategy) — and is unchanged in `master` (full diff excerpts available on request). The option began as `allowHeadersWithUnderscores: false` (per the CVE-2026-54763 record) before becoming `underscoreHeadersStrategy` and then `aliasHeadersStrategy`. The user-facing documentation describes only "request headers".
Mechanism (why names survive, and when values do too).
1. *Name pre-fill at parse time.* The client's `Trailer: X_Auth_User` declaration makes Go's server move the declared keys into `req.Trailer` with nil values before the handler runs (`net/http/transfer.go`, `fixTrailer`); HTTP/2 does the same from the `trailer:` field in the initial HEADERS ("Setup Trailers", `net/http/internal/httpcommon/httpcommon.go`). The entrypoint handlers therefore cannot see the trailer name, but the proxy forwards it. Trailer keys are canonicalized with `textproto.CanonicalMIMEHeaderKey`, which treats dashes — not underscores — as case separators: the aliasing spelling survives canonicalization as e.g. `X_auth_user` (visible in the backend dumps in PoC §1) and remains detectable by `isAliasingHeaderName`, so the fix does not depend on the client's original spelling.
2. *Value survival depends on who reads the body first.* Trailer values are appended to `req.Trailer` only while the body is consumed (`readTrailer` / `copyTrailersToHandlerRequest`). On the bare path the reverse proxy calls `Request.Clone` at handler start, before any body read, so the clone captures nil values — on HTTP/1.1 the trailer field line is then omitted entirely (`net/http/header.go`, `Header.writeSubset` writes one line per value), and h2c delivers only the empty key. When a body-buffering middleware runs first, the order reverses: the retry middleware with `status` codes buffers the body via `mirror.NewReusableRequest` → `io.ReadAll(req.Body)` (`pkg/middlewares/retry/retry.go`, `pkg/server/service/loadbalancer/mirror/mirror.go`), the values are populated before `http.Request.Clone`, and they travel to the backend. Buffering triggers for idempotent methods with `status` alone; POST additionally requires `retryNonIdempotentMethod` (both measured). Retry and buffering are the two measured paths; the mirroring and failover services use the same `mirror.NewReusableRequest` helper (`pkg/server/service/loadbalancer/mirror/mirror.go`, `failover/failover.go` when `errors.status` is configured) and share its behavior (not measured). The `buffering` middleware drains the body eagerly before the proxy too: `pkg/middlewares/buffering/buffering.go` → oxy's `multibuf.New` → `ioutil.ReadAll` (github.com/mailgun/multibuf `buffer.go`; unset limits fall back to 1 MB `DefaultMemBytes`) — measured value-preserving with default limits.
3. *Undeclared trailers: transit depends on whether anything else was declared (measured).* On HTTP/2 the standard library server copies only pre-declared trailers ("Only copy it over it was pre-declared", `net/http/internal/http2/server.go`) — undeclared fields never appear. On HTTP/1.1 `readTrailer` parses the entire trailer section with no declaration filter, and `mergeSetHeader` either rebinds the map when nil (`*dst = src`) or blindly merges when non-nil (point 4). The rebind is why zero-declaration requests lose undeclared fields at Traefik's observability `req.WithContext` shallow copy (`pkg/middlewares/observability/observability.go`, `entrypoin…
Related
Alias · CVE (1)
- CVE-2026-88004 Traefik is an open source HTTP reverse proxy and load balancer. vulnerability
References
- https://github.com/traefik/traefik/security/advisories/GHSA-v67p-phpq-fc8x web
- https://nvd.nist.gov/vuln/detail/CVE-2026-88004 advisory
- https://github.com/traefik/traefik/pull/13822 web
- https://github.com/traefik/traefik/commit/55bbda4f65e0f9c533c870983a767a7081126db7 web
- https://github.com/traefik/traefik package
- https://github.com/traefik/traefik/releases/tag/v3.7.13 web