Summary
Traefik entrypoint header-name sanitization bypassed via request trailers
Advisory details
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
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):
// 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).
- Name pre-fill at parse time. The client's
Trailer: X_Auth_Userdeclaration makes Go's server move the declared keys intoreq.Trailerwith nil values before the handler runs (net/http/transfer.go,fixTrailer); HTTP/2 does the same from thetrailer: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 withtextproto.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 byisAliasingHeaderName, so the fix does not depend on the client's original spelling. - Value survival depends on who reads the body first. Trailer values are appended to
req.Traileronly while the body is consumed (readTrailer/copyTrailersToHandlerRequest). On the bare path the reverse proxy callsRequest.Cloneat 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.writeSubsetwrites one line per value), and h2c delivers only the empty key. When a body-buffering middleware runs first, the order reverses: the retry middleware withstatuscodes buffers the body viamirror.NewReusableRequest→io.ReadAll(req.Body)(pkg/middlewares/retry/retry.go,pkg/server/service/loadbalancer/mirror/mirror.go), the values are populated beforehttp.Request.Clone, and they travel to the backend. Buffering triggers for idempotent methods withstatusalone; POST additionally requiresretryNonIdempotentMethod(both measured). Retry and buffering are the two measured paths; the mirroring and failover services use the samemirror.NewReusableRequesthelper (pkg/server/service/loadbalancer/mirror/mirror.go,failover/failover.gowhenerrors.statusis configured) and share its behavior (not measured). Thebufferingmiddleware drains the body eagerly before the proxy too:pkg/middlewares/buffering/buffering.go→ oxy'smultibuf.New→ioutil.ReadAll(github.com/mailgun/multibufbuffer.go; unset limits fall back to 1 MBDefaultMemBytes) — measured value-preserving with default limits. - 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.1readTrailerparses the entire trailer section with no declaration filter, andmergeSetHeadereither 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 observabilityreq.WithContextshallow copy (`pkg/middlewares/observability/observabilit
References
- https://github.com/advisories/GHSA-v67p-phpq-fc8x
- https://github.com/traefik/traefik/security/advisories/GHSA-v67p-phpq-fc8x
- https://nvd.nist.gov/vuln/detail/CVE-2026-88004
- https://github.com/traefik/traefik/pull/13822
- https://github.com/traefik/traefik/commit/55bbda4f65e0f9c533c870983a767a7081126db7
- https://github.com/traefik/traefik/releases/tag/v3.7.13
Related vulnerabilities
All Supply chain →- HIGHCVE-2026-100351
kin-openapi: ValidationHandler security-requirement bypass via encoded-path operation confusion
- MEDIUMGHSA-w253-m66g-rx24
Coraza: URL-encoded form Content-Type parameters bypass Coraza body inspection
- MEDIUMGHSA-3wr7-993q-jrff
Coraza: Multipart filename* (RFC 5987) charset restriction lets a decoy filename bypass FILES-based rules
- MEDIUMGHSA-g4qm-m288-5cp9
Coraza has Cookie Parser Confusion
- MEDIUMCVE-2026-107825
Coraza: ProcessURI silently drops QUERY_STRING and ARGS_GET on URI parse failure — defense-in-depth bypass for non-net/http integrations
- MEDIUMGHSA-5gj4-9gm7-2fx2
Coraza body processor has a JSON key collision that allows unauthenticated attackers to bypass OWASP CRS inspection