Update module github.com/traefik/traefik/v3 to v3.7.13 [SECURITY] - #2475
Open
devex-sa wants to merge 1 commit into
Open
Update module github.com/traefik/traefik/v3 to v3.7.13 [SECURITY]#2475devex-sa wants to merge 1 commit into
devex-sa wants to merge 1 commit into
Conversation
Contributor
Author
|
devex-sa
force-pushed
the
feature/renovate/go-github.com-traefik-traefik-v3-vulnerability
branch
from
September 11, 2026 09:33
9f72f55 to
ca5d538
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
v3.6.25→v3.7.13Traefik: respondingTimeouts.readTimeout is not applied to HTTP/3, leaving slow-body uploads unbounded
CVE-2026-88012 / GHSA-7ghq-v6jf-g56c
More information
Details
Summary
There is a medium severity vulnerability in Traefik's HTTP/3 entry points: the
respondingTimeoutssettings were not applied to the HTTP/3 request path.readTimeoutin particular is on by default at 60s and is documented as bounding the time to read the entire request including its body, but it is enforced as a deadline on the TCP connection, which cannot reach a QUIC stream, and Traefik's HTTP/3 server was constructed with no timeout of any kind. An unauthenticated client that trickles a request body therefore holds a request open for as long as it chooses, and with it one upstream connection per request, at negligible cost to itself. Backends with bounded connection pools are the practical pressure point.The HTTP/3 path lost these timeouts in v2.8.2, when a quic-go API change removed the embedded
http.Serverthat had carried them; every release from v2.8.2 onward is affected, and releases before v2.8.2 are not. Traefik v2.8.2 through v2.10.x and v3.0 through v3.6 are affected and are no longer maintained: they will not receive a patch on their own line, and the remedy for their users is to upgrade to v2.11.56 or v3.7.12.Patches
For more information
If you have any questions or comments about this advisory, please open an issue.
Original Description
Summary
entryPoints.<name>.transport.respondingTimeouts.readTimeoutis documented as:It is on by default and it works over HTTP/1.1 and HTTP/2. It has no effect on
HTTP/3.
The consequence is not that a hardening option was left unset. It is that every Traefik
deployment with
http3enabled carries a 60-second bound that the operator has everyreason to believe is in force, and which is silently absent on that protocol. A single
client trickling one body byte every few seconds holds a request open indefinitely, and
with it one upstream connection per request.
readTimeoutis applied as a deadline on the TCP connection. HTTP/3 does not haveone, and Traefik's HTTP/3 server is constructed with no timeout of any kind.
Steps to reproduce
No containers, VMs or cloud services — the official release binary,
curl,openssl,and a 68-line Python standard-library backend. Everything is attached.
By hand:
1. Static config (
conf/traefik.yml, complete and unredacted). Note there is norespondingTimeoutsblock at all — this is the documented 60s default:2. Dynamic config (
conf/dynamic.yml):3. A self-signed cert, so no CA install and no sudo:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30 -nodes \ -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"4. A backend that reads the request body before responding, as a real HTTP/1.1 server
does (
backend.py, standard library only). This matters: a backend that answers therequest headers alone lets Traefik release the upstream connection immediately, which
hides the behaviour entirely.
python3 backend.py 8080 & ./traefik --configFile=conf/traefik.yml5. The same slow upload over each protocol. The hold must exceed the timeout under
test, so 92 seconds against the 60-second default:
{ for i in $(seq 1 23); do printf 'x'; sleep 4; done; } | \ curl -v -k -T - --http1.1 https://localhost:8443/ { for i in $(seq 1 23); do printf 'x'; sleep 4; done; } | \ curl -v -k -T - --http3-only https://localhost:8443/Result
Traefik v3.7.10 (
langres, go1.26.5), officialtraefik_v3.7.10_linux_amd64.tar.gz, sha25601811bb12d44f17280550f425f5e3128d6c325f2665c09e67a651ca535f490ce.Upstream connection lifetime measured at the backend — the client's view only shows the
absence of a server action, which proves nothing on its own:
readTimeout: 5sThe HTTP/1.1 column is the control and it is the point of the exercise: the timeout is
demonstrably live on this exact binary, releasing the upstream at its configured value.
The HTTP/3 request, in the same run against the same instance with the same setting, ran
to completion with the upstream pinned throughout. Traefik returned
499on the abortedHTTP/1.1 arm and took no action at all on HTTP/3 — no
RST_STREAM, noH3_REQUEST_INCOMPLETE, no connection close.The setting is not being silently discarded: Traefik's own DEBUG log prints the loaded
static configuration including
"respondingTimeouts":{"idleTimeout":"3m0s","readTimeout":"5s"}.Full
curl -voutput, backend logs and Traefik DEBUG logs for every arm are inevidence/.Cause
readTimeoutis a TCP connection deadline —pkg/server/server_entrypoint_tcp.go:273:A deadline on a TCP connection cannot reach a QUIC stream.
And the HTTP/3 server is given no timeout of any kind —
pkg/server/server_entrypoint_tcp_http3.go:65:It reuses the HTTPS server's handler and inherits none of its timeouts. There is
therefore no duration control on the HTTP/3 request path at all — not
readTimeout, notidleTimeout, nothing.Note that this cannot be fixed by passing a field through: quic-go's
http3.Serverexposes no request-read deadline. The remedy has to be enforced around the request body
inside the handler, or upstream in quic-go.
Suggested remedy
In preference order:
readTimeouton the HTTP/3 path in the handler. Traefik already passes theHTTPS server's handler to
http3.Server, so whenRespondingTimeouts.ReadTimeout > 0it can wrap
r.Bodyfor HTTP/3 requests in a reader that enforces the deadline.http3.Serverand pass it through.operator this.
A sketch of option 1
Offered as a description of the shape, not as a patch. I have not built or tested this
against Traefik, and I am not going to present untested code as though I had. If a working,
tested patch would be useful, say so and I will prepare one properly and verify it against
the reproducer above.
Where the HTTP/3 handler is wired up —
server_entrypoint_tcp_http3.go, around thehttp3.Serverconstruction — the handler passed in could be wrapped so that, whenReadTimeoutis configured, an HTTP/3 request body carries the same deadline the TCP pathgets from
SetReadDeadline:The part worth knowing, because it is what makes the approach work rather than merely
look tidy: closing the request body cancels the QUIC stream read. In quic-go,
http3's bodyClose()callsstr.CancelRead(...), which unblocks aReadthat isalready parked waiting on the client. So a timer that closes the body on expiry bounds
both a client that trickles and a client that simply stops sending — the latter being
the case a wrapper that only checks the clock on each returning
Readwould missentirely.
Returning
os.ErrDeadlineExceededfrom the wrappedReadkeeps the failure classified asa timeout rather than a client abort, which matters for whatever status code and logging
you decide is right.
Two design questions I would not want to answer on your behalf: whether HTTP/3 should reuse
respondingTimeouts.readTimeoutor get its own setting, and whether enforcement belongs inthe handler wrapper or somewhere closer to the entrypoint. Both are your call.
A documentation issue, separately
Two things in the docs are worth correcting regardless of how the code question is
resolved.
1. The
readTimeoutdescription carries no protocol qualification. It says "themaximum duration for reading the entire request, including the body", which is exactly
what an operator relies on. The entrypoint page does note that respondingTimeouts have
"no effect for UDP entryPoints", but that does not cover this case: HTTP/3 here is served
on an HTTP entrypoint with an
http3:block, not on a Traefik UDP entrypoint,which is a separate feature for UDP routers. An operator who adds
http3:to theirexisting HTTPS entrypoint has not created a UDP entrypoint and has no reason to read that
caveat as applying to them.
2.
SECURITY.md's supported-versions table is stale. It lists3.6.xas supportedand
< 3.6.xas unsupported, while 3.7.10 is the current release. Anyone checking whethertheir version is in scope before reporting gets a confusing answer.
Impact
Each held request occupies one upstream connection for as long as the client chooses, at
negligible cost to the client, and the same client can open many. Backends with bounded
connection pools are the practical pressure point.
I have not measured a concurrency ceiling on Traefik itself, so I am not asserting
one. If that number matters to your assessment, tell me and I will measure it.
LLM ("AI") use disclosure
Not required by your policy, but stated because it is true and you should be able to
weigh it. I am a penetration tester, not a Go developer. The finding, the attack concept
and the decision to measure the upstream leg rather than the client are mine. An LLM
coding assistant (Claude) built the test harness, ran the matrix, and located the two
source citations; I verified those by hand against the
v3.7.10tag and ran thereproducer myself. I am a human and I will be the one replying in this thread.
Disclosure
Bishop Fox operates a 90-day disclosure policy, starting the day this is submitted, with
extensions where a fix is in progress. Tell me what you would prefer and I will work to
it.
Environment
traefik_v3.7.10_linux_amd64.tar.gz,sha256
01811bb12d44f17280550f425f5e3128d6c325f2665c09e67a651ca535f490ceShort summary of the problem. Make the impact and severity as clear as possible. For example: An unsafe deserialization vulnerability allows any unauthenticated user to execute arbitrary code on the server.
Details
Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer.
PoC
Complete instructions, including specific configuration details, to reproduce the vulnerability.
Impact
What kind of vulnerability is it? Who is impacted?
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Traefik: ForwardAuth identity spoofing via dot-form header alias
CVE-2026-88011 / GHSA-rf44-j88r-hh8c
More information
Details
Summary
There is a medium severity vulnerability in Traefik's handling of request headers whose name aliases another header name. Go canonicalizes header names on dashes only, so
X-Auth-User,X_Auth_UserandX.Auth.Userare three distinct headers to Traefik, while backends that derive variable names from header names (CGI, WSGI, PHP, NGINX, and others) collapse all of them into the same variable. A client can therefore smuggle an alias of a header that Traefik manages past the middleware managing it — for example a dot-formX.Authenticated.Useralongside the canonicalX-Authenticated-Userwritten by the ForwardAuth middleware — and have such a backend read the client-supplied value instead of the identity Traefik asserted. Any header Traefik sets is exposed, not only ForwardAuth's. This is an incomplete-fix sibling of GHSA-x677-9fxg-v5c5, which blocked only the underscore form.The mitigation is the new
aliasHeadersStrategyentry point option. It defaults tokeep, which preserves the previous behavior for backwards compatibility, so it must be explicitly set todeleteorrejectto take effect.Traefik v1.x, the v2 releases up to v2.11.55 and the v3 releases from v3.0.0 to v3.7.11 are affected. The unmaintained lines among them will not receive a patch of their own, and the remedy for their users is to upgrade to v2.11.56 or v3.7.12 and set
aliasHeadersStrategy.Patches
For more information
If you have any questions or comments about this advisory, please open an issue.
Original Description
Summary
Traefik's ForwardAuth middleware removes the configured canonical identity header before copying the value returned by the auth service. However, a client-supplied dot-form alias such as
X.Authenticated.Usersurvives both this replacement andunderscoreHeadersStrategy: delete.The tested PHP 8.2 built-in SAPI maps
X-Authenticated-UserandX.Authenticated.Userto the sameHTTP_X_AUTHENTICATED_USERserver variable. In Traefik's tested HTTP/1 backend path, the client value is serialized last and overrides the identity asserted by ForwardAuth.A client whom ForwardAuth permits as a lower-privilege identity can therefore be treated by the backend as another user or role.
Details
At
v3.7.10,pkg/server/server_entrypoint_tcp.go:800-818removes or rejects only names containing_. After successful authentication,pkg/middlewares/auth/forward.go:314-326deletes and replaces only the canonicalauthResponseHeaderskey. The dot alias remains inreq.Headerand the standard reverse proxy forwards both legal field names.Go's HTTP/1 writer sorts header names lexically, placing
X-Authenticated-UserbeforeX.Authenticated.User. PHP then collapses both into one$_SERVERkey, so the attacker value deterministically wins.This is an incomplete-fix sibling of
GHSA-x677-9fxg-v5c5: the published underscore input is blocked by the new entry-point strategy, while the dot input bypasses that mitigation on the current stable release.PoC
traefik-dot-forwardauth-poc.zip
run:
The decisive request is:
Expected backend identity:
lab-user, as returned by ForwardAuth.Observed on
v3.7.10:admin.The script also verifies that requests without the alias, with the canonical header, and with the already-fixed underscore alias all produce
lab-user.Impact
Applications that authorize requests using a ForwardAuth-provided identity header can receive an attacker-selected username or role instead. This was runtime-verified with PHP 8.2.27 and 8.2.33; impact on other normalization-prone backends is conditional. A lower-privilege permitted client may consequently impersonate another user or administrative role, affecting confidentiality and integrity.
This report does not claim bypass of a ForwardAuth denial: the auth service must first permit the request.
Severity
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Traefik: Rootless HTTP/1 request-target routes as "/" but is forwarded verbatim, bypassing path-scoped routing, middleware guards and access logging
CVE-2026-88009 / GHSA-f52w-8j3h-j724
More information
Details
Summary
Traefik accepts an HTTP/1.x request whose request-target is in rootless / opaque form (for example
GET http:http://internal-vhost/admin HTTP/1.1). Go parses this intoURL.Opaquewith an emptyURL.Path, so Traefik evaluates all routing, path-sanitization, middleware and access-log decisions against a path that normalizes to/, while the proxy forwards the attacker's original target byte-for-byte to the backend. Router path/prefix guards,forwardAuthpath-scoped policies and theencodedCharactershardening never see the real target, and the access log records every such request asGET / HTTP/1.1. Against a backend that resolves a rootless target as a path, this yields cross-vhost routing bypass, path-scoped authorization bypass and access-log evasion — unauthenticated, with stock entrypoint defaults.Traefik v3.0 through v3.6 are end-of-life and are also affected; they will not receive a fix on their own line. Users on those versions must 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
The scanner claims
rewriteRequestBuilder(pkg/proxy/httputil/proxy.go:97) rebuilds the outbound target fromURL.Path/RawPath/RawQuerybut never clearsURL.Opaque, so a client sending a rootless request-target (GET http:http://internal-vhost/admin HTTP/1.1) has that byte string written verbatim into the backend request line while Traefik routes, sanitizes, guards and logs an empty path.The claim is correct in every load-bearing detail, and it reproduces end to end on the GA image
traefik:v3.7(v3.7.9, go1.26.5) with stock entrypoint defaults. Three separate consequences were observed on the wire, not inferred:Host(app.example.com), nginx served theinternal-vhostserver block.forwardAuthguard that denies^/adminreturnedDENYfor/adminandALLOWfor the opaque form of the same request, which then reached/adminon the backend."GET / HTTP/1.1".Plus a fourth that is decisive against the usual closure argument: the documented opt-in hardening
encodedCharacters.allowEncodedSlash=falserejects the canonical/admin%2f..%2fsecretwith400, and does not fire at all on the opaque form carrying the identical payload.This is not the "the operator left an opt-in permissive" shape that lesson
L-012and guidelineG-03teach us to decline. The hardening is enabled and is structurally bypassed.Affected code
pkg/proxy/httputil/proxy.go:97(rewriteRequestBuilder)pkg/muxer/http/mux.go:139(withRoutingPath)Code analysis
The sink
pkg/proxy/httputil/proxy.go:87-105sets Scheme, Host, Path, RawPath, RawQuery onpr.Out.URLand clearspr.Out.RequestURI. It never touchespr.Out.URL.Opaque, whichhttputil.ReverseProxycarried over from the inbound request clone:net/http'sRequest.writethen doesruri := r.URL.RequestURI(), andurl.URL.RequestURI()returnsOpaquein preference to the escaped path wheneverOpaque != "". So the wire target is the attacker's string, and every field the proxy carefully set is ignored.How Opaque gets populated
net/http'sreadRequest($GOROOT/src/net/http/request.go:1104-1127) applies no origin-form check: it callsurl.ParseRequestURI(rawurl)directly, and the only special case isCONNECT.url.parsereturns early withOpaque = restwhenever a scheme is present and the remainder does not start with/, even forviaRequest = true. Sohttp:http://internal-vhost/adminparses to{Scheme: "http", Opaque: "http://internal-vhost/admin", Path: "", Host: ""}.Note that this string is a syntactically valid
absolute-URIper RFC 3986 (path-rootless, and:is a legalpchar), so it is a legalabsolute-formrequest-target per RFC 9112 §3.2.2 that Traefik is required to accept. The defect is not accepting it, it is rewriting it into a different URI when forwarding: Traefik receives a URI with no authority and emits one whose authority isinternal-vhost, becauseRequestURI()only re-prefixes the scheme whenOpaquebegins with//.Why the entry-point pipeline does not catch it
denyFragmentinspectsreq.URL.RawPath→ empty → passes.normalizePathreturns early whenRawPath == ""→ passes.sanitizePath(pkg/server/server_entrypoint_tcp.go:849) doesr2.URL = r2.URL.JoinPath().JoinPathdoesurl := *u, which copies Opaque, andsetPath("/"). It then doesr2.RequestURI = r2.URL.RequestURI(), which returns the Opaque string. Net effect:URL.Pathbecomes"/",Opaquesurvives untouched, andRequestURIis rewritten to the attacker's authority-bearing form.URL.Path == "/", so anyHost(...)-only orPathPrefix(/)router matches. Host matching usesreq.Host, which is theHost:header becauseURL.Hostis empty for the opaque form.encodedcharacters(pkg/middlewares/encodedcharacters/encoded_characters.go:41) scansreq.URL.EscapedPath(), which is"/". The denylist can never fire.accesslog(pkg/middlewares/accesslog/logger.go:244-253) rebuildsurlCopy := &url.URL{Path, RawPath, RawQuery, ForceQuery, Fragment}and drops Opaque, soRequestPathis logged as/.forwardauth(pkg/middlewares/auth/forward.go:473,499) setsX-Forwarded-Urifromreq.URL.RequestURI(), so the auth server receives the stringhttp://internal-vhost/admin, which matches neither the router's view (/) nor any normal path-prefix rule. It fails open against a prefix-based policy.Scope
The experimental fast proxy has the identical defect:
pkg/proxy/fast/proxy.godoesu2 := *req.URL(copying Opaque) andoutReq.SetRequestURI(u2.RequestURI())at line 216. The scanner's location call is accurate for both.Note this pattern is inherited from
net/http/httputil.ReverseProxy, whose ownNewSingleHostReverseProxydirector also leavesOpaqueset. Traefik is nevertheless the correct place to fix: it is the component that decides routing and enforces the guards that desync.Reproduction (J04, F4)
Two independent reproductions were run. All artifacts were removed afterwards (the Go probe file was deleted, all containers and the Docker network were removed; the Traefik working tree is unchanged apart from other jobs' probe files, which were left alone).
A. In-tree Go test (
pkg/server, deleted after the run)Entry-point chain assembled in
newHTTPServerorder (denyFragment→normalizePath→sanitizePath→requestdecorator→ realhttpmuxerwithHost(app.example.com)→ realhttputil.ProxyBuilder), fronted by a realnet/httpserver, driven over a raw TCP socket.Command:
Observed:
Conclusion: REPRODUCED. Traefik routes on
Path="/"andHost="app.example.com"; the backend receivesHost="internal-vhost"andPath="/admin". The%2fbytes survive to the backend'sRawPathuntouched. Theabsolute_formcontrol (GET http://internal-vhost/admin) correctly 404s, because thereURL.Hostis populated soreq.Hostbecomesinternal-vhostand the router does not match: it is specifically the rootless form, where the authority is invisible to Go'sRequest.Hostderivation but visible to the wire writer, that desyncs.B. End-to-end on the GA image (
traefik:v3.7= v3.7.9, go1.26.5) with a real nginx backendTopology: nginx with a
default_serverreturningPUBLIC-VHOSTand aserver_name internal-vhostblock returningINTERNAL-VHOST-SECRET; Traefik with a singleHost(app.example.com)router, entry-point defaults,--accesslog=true. Requests sent over a raw socket withHost: app.example.com.B1. Cross-vhost + log evasion (stock defaults):
Traefik access log for those same three requests:
B2. Differential against the documented hardening (
--entrypoints.web.http.encodedCharacters.allowEncodedSlash=false,sanitizePath=true):B3. ForwardAuth authorization bypass (middleware
forwardAuthto an nginx auth service that returns 403 whenX-Forwarded-Urimatches^/admin):Auth-service log confirms the decision flip:
403,403,200.Conclusion: REPRODUCED on a GA release artifact. The primitive is unauthenticated, needs no non-default configuration, and yields cross-vhost selection, path-scoped authorization bypass, and complete access-log evasion simultaneously.
Documentation grounding
Governing page:
docs/content/security/request-path.md(published as https://doc.traefik.io/traefik/security/request-path/). Not WAI.(truncated ; full analysis in the linked internal report)
Reproduction (J18, F20)
Three Go probes were written into
pkg/server/of the checkout (namedzz_scanpoc_J18*_test.go) and deleted afterwards;git statusconfirms nozz_scanpoc_J18file remains and the checkout is still onv3.7@d5072ce7b8765c9574246072e05dd81d84950da7. Docker containers were removed at the end of the run.Probe 1 — routing desync and verbatim forward. Real entry point chain (
denyFragment->normalizePath->sanitizePath->requestdecorator->httpmuxer.Muxer), two routers on the same service, realpkg/proxy/httputilproxy, raw TCP backend recording the request line, driven over a raw socket.The control shows the deployment is correctly guarded for a well-formed request; the rootless form reaches the unguarded router and the backend receives the attacker's bytes, including the
%2Fthat anencodedCharactersfilter would have rejected.Probe 2 — origin tolerance. Which origins actually resolve a rootless request-target.
GET admin/secret HTTP/1.1GET /admin/secret HTTP/1.1(control)net/httpHTTP/1.1 400 Bad RequestPath="/admin/secret"HTTP/1.1 400 Bad RequestHTTP/1.1 400 Bad RequestHTTP/1.1 400 Bad RequestHTTP/1.1 200 OKHTTP/1.1 400http.server200 OK,Path="/admin/secret"Path="/admin/secret"fasthttp also decodes the encoded form:
GET admin%2Fsecret HTTP/1.1yieldsPath="/admin/secret",RequestURI="admin%2Fsecret".Probe 3 — end-to-end authentication bypass. Same chain as probe 1, with a real
basicAuth-style gate on the/adminrouter and afasthttporigin servingADMIN_PANEL_SECRETat/admin/secret.The bypass is real: the credentialed path returns 401, the malformed path returns the protected content with no credentials.
Second affected site (J18)
The finding is mechanically correct and fully reproduced end to end, including the auth bypass.
A client-controlled HTTP/1.x request-target of the form
scheme:rootless/path(for exampleGET http:admin/secret HTTP/1.1) is parsed by Go'surl.ParseRequestURIintoURL.Opaque = "admin/secret"with an emptyURL.Path/URL.RawPath. Traefik's entry point chain and muxer never look atURL.Opaque:denyFragmentinspectsURL.RawPath(empty) and passes.normalizePathreturns early on emptyRawPath.sanitizePathcallsURL.JoinPath(), which rewritesPathto"/"and leavesOpaqueuntouched, then setsRequestURI = URL.RequestURI()="admin/secret".withRoutingPath(pkg/muxer/http/mux.go:139) derives the routing path fromreq.URL.EscapedPath(), which ignoresOpaque, so everyPath/PathPrefix/PathRegexpmatcher evaluates against"/".Opaque, so the outgoing request line is the attacker's target verbatim.Result: Traefik makes its routing and middleware decision on one string (
"/") and writes a different string to the backend (admin/secret). Where a host-only orPathPrefix("/")router reaches the same service as a path-guarded router, the guarded router is skipped, and a lenient origin resolves the rootless target as an absolute path.Where the scanner overstates: it presents the exploit scenario as if the lenient-origin precondition were incidental. It is the whole exposure. Of the seven origin implementations tested, five reject the rootless target with 400 (Go
net/http, nginx, Apache httpd, Node.js/llhttp, Tomcat 9). Onlyfasthttp(and the Fiber family built on it) and Python'shttp.serveraccept it. Notably Tomcat, the backend family that carried the closest prior report (GHSA-vrvv-46fp-28pp), answers 400 here.Documentation grounding
Governing page:
docs/content/security/request-path.md(published as https://doc.traefik.io/traefik/security/request-path/). Not WAI.The page documents the entry-point path pipeline as three stages (encoded-character filtering, path normalization, path sanitization) and presents
sanitizePath: trueas a default-on hardening the team ships, withencodedCharacters.allowEncodedSlash: falseas the opt-in tightening for backends that decode reserved characters. Nothing on this page, nor onheader-underscores.md,content-length.md,http2-header-memory.mdormulti-tenant-kubernetes.md, documents the request-target form, absolute-form / rootless targets,URL.Opaque, or an authority carried in the target. Grep forabsolute,request-target,request line,Opaque,authorityacrossdocs/content/security/returns nothing.This lands squarely in Step 2e's second bucket, not the first: a behaviour documented as a default-on protection, with a sibling code path that structurally escapes it. Evidence B2 is the discriminator, and it is exactly the GHSA-cxjq shape (undocumented gap defeating a shipped guard) rather than the GHSA-x9c2 shape (documented behaviour with an opt-in the operator declined to enable). Here the operator did enable the opt-in and it still failed.
Precedent in comparable projects
Searched
data/competitors/*.jsononabsolute.form|absolute-form|absolute URI|request.target|request line|authority.form, then onsmuggl|desync|normaliz.MatchPath's%xx(escaped-path) branch skips case normalization, so the matcher's view of the path diverges from the served one, enabling path-based route/auth bypass.MatchHostbecomes case-sensitive above 100 hosts, so host matching diverges from the request's real host, enabling host-based route/auth bypass.#fragmenttreated as part of the path element causes the authorization filter and the router to disagree, bypassing authz policy.%2Fbefore matching.(truncated ; full analysis in the linked internal report)
Recommended fix
Assign for fix, and treat as CVE-worthy.
pkg/proxy/httputil/proxy.go, next to the Path/RawPath assignments:pkg/proxy/fast/proxy.go, on theu2 := *req.URLcopy beforeoutReq.SetRequestURI(u2.RequestURI()). This alone closes the forwarding half.CONNECTrequests, requirereq.URL.Opaque == ""and anEscapedPath()beginning with/, or normalize the trueabsolute-formcase by promoting the authority intoreq.Host. This makes the router, the path sanitizers, the middlewares, the access log and the backend agree on a single interpretation of the target, which step 1 alone does not achieve: without it,sanitizePathstill rewritesRequestURIto the attacker's authority-bearing form and the access log still records/.ReverseProxyomission upstream to Go as well, sinceNewSingleHostReverseProxyhas the same gap, but do not make the Traefik fix wait on it.opaque-request-target-forwardingand note that the fix must land on the fast proxy in the same PR.Provenance
Found by an external automated code scan (
CLAUDE-SECURITY-20260824-122205) ofpkg/middlewares,pkg/proxy,pkg/server,pkg/muxerandpkg/tlson branchv3.7at commitd5072ce7b8765c9574246072e05dd81d84950da7, then triaged with theadvisory-checkprocess : mechanism-level duplicate check against the existing advisory corpus, CVE-policy gate, security-documentation grounding, comparable-project precedent, and a mandatory reproduction attempt.Triage outcome : Likely Valid, confidence High, reproduced (yes). Expected publication likelihood at triage time : High.
Scanner finding ids : F4, F20. Internal report :
findings/scan-20260824/verdicts/J04.md, J18.mdin the security-advisor repository.Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Traefik HTTP/3 Backend NTLM Connection Reuse
CVE-2026-88007 / GHSA-qqjf-53cj-pwvv
More information
Details
Summary
Traefik's HTTP/3 request path did not initialize the connection-scoped backend transport holder that isolates connection-bound NTLM and Negotiate (Kerberos) authentication on the HTTP/1.1 and HTTP/2 paths. The HTTP/3 entrypoint reuses the HTTPS handler chain and reaches the same backend round-tripper, but its
ConnContextnever calledservice.AddTransportOnContext, sokerberosRoundTripperfell back to the shared backend transport instead of a per-frontend-connection pool. On a route served over HTTP/3 to a backend that binds identity to a persistent connection via NTLM or Negotiate, an unrelated HTTP/3 client could be assigned a backend connection already authenticated as a victim and inherit that identity, reading victim-only data and performing actions as the victim without presenting the victim's credentials. Affected deployments require HTTP/3 enabled on the entrypoint, a backend using connection-bound NTLM/Negotiate authentication, and backend keep-alive; deployments using ordinary per-request authentication are not affected.Patches
For more information
If you have any questions or comments about this advisory, please open an issue.
Original Description
Traefik HTTP/3 Backend NTLM Connection Reuse
Summary
Traefik's HTTP/3 request path does not initialize the connection-scoped backend transport state that Traefik uses to isolate connection-bound NTLM and Negotiate authentication for HTTP/1.1 and HTTP/2. When a backend keeps authenticated identity on a persistent HTTP/1.1 TCP connection, an unrelated HTTP/3 client can reuse a victim-authenticated backend connection and inherit that backend identity.
In the attached reproduction, the HTTPS/HTTP/1.1 control case behaves correctly and isolates the attacker, but the HTTP/3 case allows a second unauthenticated client to read victim-only data and execute a state-changing request as
actor=victim.Validated target:
traefik/traefikf2d0794417e4d06343e6e7c4722143f5b34bee452026-08-25T06:48:02Z2026-08-24T08:26:06ZDetails
The issue is caused by a protocol-parity gap between the normal TCP HTTP entrypoint path and the HTTP/3 entrypoint path.
For HTTP/1.1 and HTTP/2, Traefik explicitly creates a connection-scoped holder that can later store a dedicated RoundTripper for NTLM or Negotiate:
That helper installs the per-connection holder, and
kerberosRoundTripperdepends on it. If the holder is absent, it falls back to the shared original backend transport. If NTLM or Negotiate is detected, it stores a dedicated cloned RoundTripper into that holder so future requests stay on the authenticated backend connection:For HTTP/3, the server reuses the normal HTTPS handler chain, but its
ConnContextonly propagates the TLS options name and does not callservice.AddTransportOnContext:This means HTTP/3 requests reach the same reverse-proxy and backend transport logic as HTTPS, but without the connection-scoped transport holder that NTLM and Negotiate isolation relies on.
In practice, the flow is:
transportKey,kerberosRoundTripperuses the sharedOriginalRoundTripper.The attached verifier demonstrates both the negative control and the exploit path:
401Authorizationheader, readsresource=secret actor=victim, executesaction=transfer actor=victim to=attacker amount=5000, and hits the same backend TCP connection identifier as the victimPoC
See the reproduction materials at:
https://gist.github.com/OneZ3r0/41da8e8b79ebbe444a94f8a2a3a30895
The gist can also be downloaded as a ZIP archive.
Files included in this gist:
run.shDockerfile.dockerignorego.modgo.sumverify.goThe package is intentionally kept as a single-container reproduction:
run.shbuilds a local image for the pinned target commitRun:
run.shdefaults to the validated commit above. To override it explicitly:Expected terminal result:
Important observed behavior from the PoC:
401Authorization/transfer?to=attacker&amount=5000asactor=victimEnvironment notes:
Impact
This is a cross-client authorization bypass affecting deployments that expose HTTP/3 routes to backends using connection-bound NTLM or Negotiate authentication with persistent backend connection reuse.
In the verified reproduction, an unauthenticated second client can:
Attack prerequisites:
Deployments using ordinary per-request authentication are not affected by this specific issue.
Severity
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Traefik entrypoint header-name sanitization bypassed via request trailers
CVE-2026-88004 / GHSA-v67p-phpq-fc8x
More information
Details
Summary
Traefik's entrypoint defenses against spoofed trusted header names —
aliasHeadersStrategy/underscoreHeadersStrategyindeleteorrejectmode, and the defaultforwardedHeadersstripping of client-suppliedX-Forwarded-*— scanreq.Headeronly and neverreq.Trailer. An unauthenticated client can therefore smuggle a sanitized name (an aliasing spelling such asX_Auth_User, or a trusted name such asX-Forwarded-Prefix) as an HTTP/1.1 chunked trailer or an HTTP/2 trailer:rejectdoes not return its documented400,deletedoes 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 (theretrymiddleware with status codes, or thebufferingmiddleware) 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'shttputil.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.