Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions traefik/CVE-2026-48020/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Traefik - StripPrefix Middleware Authentication Bypass (CVE-2026-48020)

## Overview

Traefik versions prior to `2.11.48`, `3.6.19`, and `3.7.3` contain a path normalization flaw in the `StripPrefix` middleware. When a public router using `StripPrefix` shares a backend with protected routes (such as `/admin` or `/internal` guarded by authentication middleware), improper handling of relative path segments after prefix stripping can allow requests matched by the public router to resolve to protected backend paths without passing through the protected router's authentication middleware.

- **Vulnerability Type**: Authentication Bypass / Path Normalization Mismatch in `StripPrefix` Middleware
- **Severity**: Critical
- **Vulnerable Versions**: `< 2.11.48`, `3.0.0 - 3.6.18`, `3.7.0 - 3.7.2` (e.g., `v3.7.0`)
- **Patched Versions**: `2.11.48`, `3.6.19`, `3.7.3` or later

---

## Testbed Setup

The testbed uses Docker Compose to deploy two Traefik reverse proxy instances fronting a shared Python HTTP backend (`cve_backend` on port `9000`):
- **Vulnerable instance (`traefik_vulnerable_lab`)**: `traefik:v3.7.0` running on host port `18080`.
- **Patched / Safe instance (`traefik_patched_lab`)**: `traefik:v3.7.3` running on host port `18081`.
- **Backend service (`cve_backend`)**: Python `3.12-slim` application ([`backend.py`](./backend.py)) configured via [`dynamic.yml`](./dynamic.yml) with:
- A `public` router matching `PathPrefix(\`/api\`)` with the `strip-api` (`StripPrefix: /api`) middleware.
- A `protected` router matching `PathPrefix(\`/admin\`) || PathPrefix(\`/internal\`)` guarded by `BasicAuth` (`auth`) middleware.

### Start the Testbed

```bash
cd /usr/local/google/home/dyeddala/tsunami/security-testbeds/traefik/CVE-2026-48020

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The path to the testbed directory is hardcoded to a specific user's local home directory (/usr/local/google/home/dyeddala/...). This should be updated to a relative path so that the instructions work for any user.

Suggested change
cd /usr/local/google/home/dyeddala/tsunami/security-testbeds/traefik/CVE-2026-48020
cd traefik/CVE-2026-48020

docker compose up -d
```

### Stop the Testbed

```bash
docker compose down -v
```

---

## Verification

### 1. Verify Endpoint Readiness

#### Step 1: Verify the Traefik instances and public `/api` route are active
```bash
curl -i -s "http://127.0.0.1:18080/api" | head -n 15
curl -i -s "http://127.0.0.1:18081/api" | head -n 15
```
**Expected Response**: `HTTP/1.1 404 Not Found` with JSON body `{"status": "not_found", "seen_path": "/"}` from the upstream Python backend, confirming `/api` was stripped and forwarded to `backend:9000`.

#### Step 2: Verify direct access to `/admin` requires authentication on both instances
```bash
curl -i -s "http://127.0.0.1:18080/admin" | head -n 15
curl -i -s "http://127.0.0.1:18081/admin" | head -n 15
```
**Expected Response**: `HTTP/1.1 401 Unauthorized` (with `Www-Authenticate: Basic realm="traefik"`), confirming the `protected` router's `BasicAuth` middleware blocks unauthenticated direct access.

---

### 2. Automated Side-by-Side Verification with Tsunami

```bash
for port in 18080 18081; do
echo "=== Scanning Port $port ==="
docker run --rm --net=host \
--entrypoint /usr/bin/tsunami \
-v /tmp:/tmp \
ghcr.io/google/tsunami-scanner-full \
--ip-v4-target=127.0.0.1 \
--detectors-include=Traefik_CVE_2026_48020 \
--port-ranges-target="${port}" \
--scan-results-local-output-format=JSON \
--scan-results-local-output-filename="/tmp/tsunami-out-${port}.json" 2>&1 | grep "# of detected vulnerability:"
done
``

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code block is closed with only two backticks instead of three, which causes a markdown rendering syntax error. Please add a third backtick to properly close the code block.

28 changes: 28 additions & 0 deletions traefik/CVE-2026-48020/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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.end_headers()
self.wfile.write(body)

def do_GET(self):
print(f"DEBUG: Request line: {self.requestline}, path: {self.path}")
if "/admin" in self.path or "/internal" in self.path:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current path check uses "/admin" in self.path, which can lead to false positives if the string "/admin" or "/internal" appears in the query parameters (e.g., ?file=/admin) of a public request. Splitting the path by ? to exclude query parameters will make the verification more robust.

Suggested change
if "/admin" in self.path or "/internal" in self.path:
if "/admin" in self.path.split("?")[0] or "/internal" in self.path.split("?")[0]:

self._json(200, {
"status": "success",
"seen_path": self.path,
"message": "Reached endpoint successfully"
})
else:
self._json(404, {"status": "not_found", "seen_path": self.path})

if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 9000), Handler)
server.serve_forever()
36 changes: 36 additions & 0 deletions traefik/CVE-2026-48020/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
services:
traefik_vulnerable:
image: traefik:v3.7.0
container_name: traefik_vulnerable_lab
command:
- "--providers.file.filename=/etc/traefik/dynamic.yml"
- "--entrypoints.web.address=:8080"
ports:
- "18080:8080"
volumes:
- ./dynamic.yml:/etc/traefik/dynamic.yml:ro
depends_on:
- backend

traefik_patched:
image: traefik:v3.7.3
container_name: traefik_patched_lab
command:
- "--providers.file.filename=/etc/traefik/dynamic.yml"
- "--entrypoints.web.address=:8080"
ports:
- "18081:8080"
volumes:
- ./dynamic.yml:/etc/traefik/dynamic.yml:ro
depends_on:
- backend

backend:
image: python:3.12-slim
container_name: cve_backend
working_dir: /app
environment:
- PYTHONUNBUFFERED=1
command: python /app/backend.py
volumes:
- ./backend.py:/app/backend.py:ro
33 changes: 33 additions & 0 deletions traefik/CVE-2026-48020/dynamic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
http:
routers:
public:
rule: "PathPrefix(`/api`)"
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:{PLAIN}test"

services:
backend:
loadBalancer:
servers:
- url: "http://backend:9000"
Loading