PT-2026-89455 · Go+2 · Github.Com/Traefik/Traefik/V2+2
CVE-2026-88007
·
Published
2026-09-10
·
Updated
2026-09-11
CVSS v4.0
9.1
Critical
| Vector | AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N |
Summary
Traefik's HTTP/3 request path did not initialize the connection-scoped backend transport holder that isolates connection-bound NTLM and Negotiate (Kerberos) authentication on the HTTP/1.1 and HTTP/2 paths. The HTTP/3 entrypoint reuses the HTTPS handler chain and reaches the same backend round-tripper, but its
ConnContext never called service.AddTransportOnContext, so kerberosRoundTripper fell back to the shared backend transport instead of a per-frontend-connection pool. On a route served over HTTP/3 to a backend that binds identity to a persistent connection via NTLM or Negotiate, an unrelated HTTP/3 client could be assigned a backend connection already authenticated as a victim and inherit that identity, reading victim-only data and performing actions as the victim without presenting the victim's credentials. Affected deployments require HTTP/3 enabled on the entrypoint, a backend using connection-bound NTLM/Negotiate authentication, and backend keep-alive; deployments using ordinary per-request authentication are not affected.Patches
- https://github.com/traefik/traefik/releases/tag/v2.11.57
- 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.
Original Description
Traefik HTTP/3 Backend NTLM Connection Reuse
Summary
Traefik's HTTP/3 request path does not initialize the connection-scoped backend transport state that Traefik uses to isolate connection-bound NTLM and Negotiate authentication for HTTP/1.1 and HTTP/2. When a backend keeps authenticated identity on a persistent HTTP/1.1 TCP connection, an unrelated HTTP/3 client can reuse a victim-authenticated backend connection and inherit that backend identity.
In the attached reproduction, the HTTPS/HTTP/1.1 control case behaves correctly and isolates the attacker, but the HTTP/3 case allows a second unauthenticated client to read victim-only data and execute a state-changing request as
actor=victim.Validated target:
- Repository:
traefik/traefik - Commit:
f2d0794417e4d06343e6e7c4722143f5b34bee45 - Validation time:
2026-08-25T06:48:02Z - Commit time:
2026-08-24T08:26:06Z - Patched status: not evaluated
Details
The issue is caused by a protocol-parity gap between the normal TCP HTTP entrypoint path and the HTTP/3 entrypoint path.
For HTTP/1.1 and HTTP/2, Traefik explicitly creates a connection-scoped holder that can later store a dedicated RoundTripper for NTLM or Negotiate:
go
// pkg/server/server entrypoint tcp.go:691-703
var connContext multipleConnContext
connContext.AddConnContextFunc(func(ctx context.Context, c net.Conn) context.Context {
// This adds an empty struct in order to store a RoundTripper in the ConnContext in case of Kerberos or NTLM.
ctx = service.AddTransportOnContext(ctx)
if tlsConn, ok := c.(*tls.Conn); ok {
if tlsConnWithOptionsName, ok := tlsConn.NetConn().(tcp.TLSConn); ok {
return tcp.AddTLSOptionsNameInContext(ctx, tlsConnWithOptionsName.TLSOptionsName)
}
}
return ctx
})That helper installs the per-connection holder, and
kerberosRoundTripper depends on it. If the holder is absent, it falls back to the shared original backend transport. If NTLM or Negotiate is detected, it stores a dedicated cloned RoundTripper into that holder so future requests stay on the authenticated backend connection:go
// pkg/server/service/transport.go:374-402
func AddTransportOnContext(ctx context.Context) context.Context {
return context.WithValue(ctx, transportKey, &stickyRoundTripper{})
}
type kerberosRoundTripper struct {
new func() http.RoundTripper
OriginalRoundTripper http.RoundTripper
}
func (k *kerberosRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
value, ok := request.Context().Value(transportKey).(*stickyRoundTripper)
if !ok {
return k.OriginalRoundTripper.RoundTrip(request)
}
if value.RoundTripper != nil {
return value.RoundTripper.RoundTrip(request)
}
resp, err := k.OriginalRoundTripper.RoundTrip(request)
// If we found that we are authenticating with Kerberos (Negotiate) or NTLM.
// We put a dedicated roundTripper in the ConnContext.
// This will stick the next calls to the same connection with the backend.
if err == nil && containsNTLMorNegotiate(resp.Header.Values("WWW-Authenticate")) {
value.RoundTripper = k.new()
}
return resp, err
}For HTTP/3, the server reuses the normal HTTPS handler chain, but its
ConnContext only propagates the TLS options name and does not call service.AddTransportOnContext:go
// pkg/server/server entrypoint tcp http3.go:65-80
h3.Server = &http3.Server{
Addr: config.GetAddress(),
Port: config.HTTP3.AdvertisedPort,
Handler: httpsServer.Server.(*http.Server).Handler,
TLSConfig: &tls.Config{GetConfigForClient: h3.getTLSConfigForClient},
QUICConfig: &quic.Config{
Allow0RTT: false,
},
ConnContext: func(ctx context.Context, c *quic.Conn) context.Context {
tlsOptionsName, err := h3.getTLSOptionsName(c)
if err != nil {
log.Error().Msgf("Error getting TLS options name for client: %v", err)
return ctx
}
return tcp.AddTLSOptionsNameInContext(ctx, tlsOptionsName)
},
}This means HTTP/3 requests reach the same reverse-proxy and backend transport logic as HTTPS, but without the connection-scoped transport holder that NTLM and Negotiate isolation relies on.
In practice, the flow is:
- A victim authenticates through Traefik to a backend that binds identity to the backend TCP connection using NTLM or Negotiate.
- Because the HTTP/3 request context does not contain
transportKey,kerberosRoundTripperuses the sharedOriginalRoundTripper. - No frontend-connection-specific dedicated backend pool is installed for that HTTP/3 client.
- A second unrelated HTTP/3 client can be assigned the same backend TCP connection after the victim has authenticated it.
- That second client inherits the victim's backend identity without sending the victim's credentials.
The attached verifier demonstrates both the negative control and the exploit path:
- HTTPS/HTTP/1.1 control case: the attacker uses a separate frontend connection and correctly receives
401 - HTTP/3 exploit case: the attacker uses a separate HTTP/3 client with no
Authorizationheader, readsresource=secret actor=victim, executesaction=transfer actor=victim to=attacker amount=5000, and hits the same backend TCP connection identifier as the victim
PoC
See the reproduction materials at:
https://gist.github.com/OneZ3r0/41da8e8b79ebbe444a94f8a2a3a30895
The gist can also be downloaded as a ZIP archive.
Files included in this gist:
run.shDockerfile.dockerignorego.modgo.sumverify.go
The package is intentionally kept as a single-container reproduction:
run.shbuilds a local image for the pinned target commit- the Dockerfile builds both Traefik and the verifier during image build
- the container runs the verifier directly as its entrypoint
- the verifier starts a synthetic backend, launches Traefik, runs the HTTPS/HTTP/1.1 control case, then runs the HTTP/3 exploit case
Run:
bash
./run.shrun.sh defaults to the validated commit above. To override it explicitly:bash
PRODUCT COMMIT=f2d0794417e4d06343e6e7c4722143f5b34bee45 ./run.shExpected terminal result:
text
REPRODUCED: HTTP/1.1 isolates the authenticated backend connection, but HTTP/3 reuses the victim-authenticated backend connection for a different client and executes an unauthorized state-changing request as the victim.Important observed behavior from the PoC:
- the HTTP/1.1 control case succeeds only if a fresh attacker connection receives
401 - the HTTP/3 exploit case succeeds only if the attacker reads victim-only data without sending
Authorization - the HTTP/3 exploit case succeeds only if the attacker performs
/transfer?to=attacker&amount=5000asactor=victim - the HTTP/3 exploit case succeeds only if the attacker uses the same backend TCP connection identifier as the victim
Environment notes:
- Docker is required
- the build fetches the target Traefik source from GitHub
- no production credentials or external NTLM service are required; the verifier includes a synthetic NTLM-like backend specifically to demonstrate connection-bound identity reuse
Impact
This is a cross-client authorization bypass affecting deployments that expose HTTP/3 routes to backends using connection-bound NTLM or Negotiate authentication with persistent backend connection reuse.
In the verified reproduction, an unauthenticated second client can:
- read victim-only data
- perform a state-changing action as the victim
- reuse a backend TCP connection that has already been authenticated as the victim
Attack prerequisites:
- HTTP/3 enabled on the Traefik entrypoint
- a routed backend using connection-bound NTLM or Negotiate authentication
- backend keep-alive and backend connection reuse enabled
- the attacker can reach the same route as the victim
Deployments using ordinary per-request authentication are not affected by this specific issue.
Exploit
Fix
Improper Authentication
Incorrect Authorization
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Github.Com/Traefik/Traefik/V2
Github.Com/Traefik/Traefik/V3
Traefik