Summary
Traefik: Rootless HTTP/1 request-target routes as "/" but is forwarded verbatim, bypassing path-scoped routing, middleware guards and access logging
Advisory 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 into URL.Opaque with an empty URL.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, forwardAuth path-scoped policies and the encodedCharacters hardening never see the real target, and the access log records every such request as GET / 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
- https://github.com/traefik/traefik/releases/tag/v2.11.57
- 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
The scanner claims rewriteRequestBuilder (pkg/proxy/httputil/proxy.go:97) rebuilds the outbound target from URL.Path / RawPath / RawQuery but never clears URL.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:
- Cross-vhost routing bypass. Traefik matched
Host(app.example.com), nginx served theinternal-vhostserver block. - Path-scoped authorization bypass. A
forwardAuthguard that denies^/adminreturnedDENYfor/adminandALLOWfor the opaque form of the same request, which then reached/adminon the backend. - Access-log evasion. All three requests, benign and malicious, were logged identically as
"GET / HTTP/1.1".
Plus a fourth that is decisive against the usual closure argument: the documented opt-in hardening encodedCharacters.allowEncodedSlash=false rejects the canonical /admin%2f..%2fsecret with 400, 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-012 and guideline G-03 teach 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-105 sets Scheme, Host, Path, RawPath, RawQuery on pr.Out.URL and clears pr.Out.RequestURI. It never touches pr.Out.URL.Opaque, which httputil.ReverseProxy carried over from the inbound request clone:
pr.Out.URL.Scheme = target.Scheme
pr.Out.URL.Host = target.Host
...
pr.Out.URL.Path = u.Path
pr.Out.URL.RawPath = u.RawPath
...
pr.Out.RequestURI = "" // Outgoing request should not have RequestURI
net/http's Request.write then does ruri := r.URL.RequestURI(), and url.URL.RequestURI() returns Opaque in preference to the escaped path whenever Opaque != "". So the wire target is the attacker's string, and every field the proxy carefully set is ignored.
How Opaque gets populated
net/http's readRequest ($GOROOT/src/net/http/request.go:1104-1127) applies no origin-form check: it calls url.ParseRequestURI(rawurl) directly, and the only special case is CONNECT. url.parse returns early with Opaque = rest whenever a scheme is present and the remainder does not start with /, even for viaRequest = true. So http:http://internal-vhost/admin parses to {Scheme: "http", Opaque: "http://internal-vhost/admin", Path: "", Host: ""}.
Note that this string is a syntactically valid absolute-URI per RFC 3986 (path-rootless, and : is a legal pchar), so it is a legal absolute-form request-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 is internal-vhost, because RequestURI() only re-prefixes the scheme when Opaque begins 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.- The muxer matches on
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.go does u2 := *req.URL (copying Opaque) and outReq.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 own NewSingleHostReverseProxy director also leaves Opaque set. 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 newHTTPServer order (denyFragment → normalizePath → sanitizePath → requestdecorator → real httpmuxer with Host(app.example.com) → real httputil.ProxyBuilder), fronted by a real net/http server, driven over a raw TCP socket.
Command:
go test -run TestScanPocJ04Opaque -v ./pkg/server/
Observed:
=== RUN TestScanPocJ04Opaque/control_origin_form
status="200 OK" reachedBackend=true backend.RequestURI="/hello" backend.Host="app.example.com"
=== RUN TestScanPocJ04Opaque/rootless_opaque_form
status="200 OK" reachedBackend=true
routed(URL.Path="/" RawPath="" Opaque="http://internal-vhost/admin%2f..%2fsecret" RequestURI="http://internal-vhost/admin%2f..%2fsecret" Host="app.example.com")
backend(RequestURI="http://internal-vhost/admin%2f..%2fsecret" Host="internal-vhost" Path="/admin/../secret" RawPath="/admi
References
- https://github.com/advisories/GHSA-f52w-8j3h-j724
- https://github.com/traefik/traefik/security/advisories/GHSA-f52w-8j3h-j724
- https://nvd.nist.gov/vuln/detail/CVE-2026-88009
- https://github.com/traefik/traefik/pull/13796
- https://github.com/traefik/traefik/commit/58d1e9ca204526823211e30fd4634101c59d58e9
- https://github.com/traefik/traefik/releases/tag/v2.11.57
- https://github.com/traefik/traefik/releases/tag/v3.7.13
Related vulnerabilities
All Supply chain →- CRITICALCVE-2026-88092
Microsoft Security Advisory CVE-2026-88092 - ASP.NET Core Security Feature Bypass Vulnerability
- CRITICALCVE-2026-70188
Microsoft Security Advisory CVE-2026-70188 - ASP.NET Core Security Feature Bypass Vulnerability
- MEDIUMCVE-2026-85078
sanic chunked trailer request smuggling allows hidden second request execution
- MEDIUMCVE-2026-69216
Http4s: Ember chunk parser lenience (TE.TE request smuggling)
- HIGHCVE-2026-69205
Http4s Ember Transfer-Encoding value parsing (TE.CL / TE.0 request smuggling)
- CRITICALCVE-2026-69204
Http4s Ember accepts Transfer-Encoding combined with Content-Length (CL.TE request smuggling)