GitHub Advisoryadvisory · Go

GHSA-f52w-8j3h-j724 Traefik: Rootless HTTP/1 request-target routes as "/" but is forwarded verbatim, bypassing path-scoped routing, middleware guards and access logging

HIGH Published 2026-09-10 Modified 2026-09-10 Official page ↗
Aliases
CVE-2026-88009
Ecosystem
Go
Packages
github.com/traefik/traefik/v3, github.com/traefik/traefik/v2
Severity
High
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
Weaknesses
CWE-1286, CWE-444
Reviewed
2026-09-10

Affected packages

• Go github.com/traefik/traefik/v3: >= 3.0.0, < 3.7.13 → fixed in 3.7.13 • Go github.com/traefik/traefik/v2: < 2.11.57 → fixed in 2.11.57

Description

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: 1. Cross-vhost routing bypass. Traefik matched `Host(app.example.com)`, nginx served the `internal-vhost` server block. 2. Path-scoped authorization bypass. A `forwardAuth` guard that denies `^/admin` returned `DENY` for `/admin` and `ALLOW` for the opaque form of the same request, which then reached `/admin` on the backend. 3. 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: ```go 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 • `denyFragment` inspects `req.URL.RawPath` → empty → passes. • `normalizePath` returns early when `RawPath == ""` → passes. • `sanitizePath` (`pkg/server/server_entrypoint_tcp.go:849`) does `r2.URL = r2.URL.JoinPath()`. `JoinPath` does `url := *u`, which copies Opaque, and `setPath("/")`. It then does `r2.RequestURI = r2.URL.RequestURI()`, which returns the Opaque string. Net effect: `URL.Path` becomes `"/"`, `Opaque` survives untouched, and `RequestURI` is *rewritten to the attacker's authority-bearing form*. • The muxer matches on `URL.Path == "/"`, so any `Host(...)`-only or `PathPrefix(`/`)` router matches. Host matching uses `req.Host`, which is the `Host:` header because `URL.Host` is empty for the opaque form. • `encodedcharacters` (`pkg/middlewares/encodedcharacters/encoded_characters.go:41`) scans `req.URL.EscapedPath()`, which is `"/"`. The denylist can never fire. • `accesslog` (`pkg/middlewares/accesslog/logger.go:244-253`) rebuilds `urlCopy := &url.URL{Path, RawPath, RawQuery, ForceQuery, Fragment}` and drops Opaque, so `RequestPath` is logged as `/`. • `forwardauth` (`pkg/middlewares/auth/forward.go:473,499`) sets `X-Forwarded-Uri` from `req.URL.RequestURI()`, so the auth server receives the string `http://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="/admin%2f..%2fsecret") === RUN TestScanPocJ0…

Related

Alias · CVE (1)

  • CVE-2026-88009 Traefik is an open source HTTP reverse proxy and load balancer. vulnerability

Weakness · CWE (2)

  • CWE-1286 Improper Validation of Syntactic Correctness of Input weakness
  • CWE-444 Inconsistent Interpretation of HTTP Requests weakness

References