Résumé
Traefik HTTP/3 Backend NTLM Connection Reuse
Détails de l’avis
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:
// 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:
// 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:
// 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:
./run.sh
run.sh defaults to the validated commit above. To override it explicitly:
PRODUCT_COMMIT=f2d0794417e4d06343e6e7c4722143f5b34bee45 ./run.sh
Expected terminal result:
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
Références
- https://github.com/advisories/GHSA-qqjf-53cj-pwvv
- https://github.com/traefik/traefik/security/advisories/GHSA-qqjf-53cj-pwvv
- https://nvd.nist.gov/vuln/detail/CVE-2026-88007
- https://github.com/traefik/traefik/pull/13812
- https://github.com/traefik/traefik/commit/ff39c47d7459dec9cd8de63c1a4e7aa7315bdc1c
- https://github.com/traefik/traefik/releases/tag/v2.11.57
- https://github.com/traefik/traefik/releases/tag/v3.7.13
Vulnérabilités liées
Tout Supply chain →- CRITICALSC-GHA-OIDC-MISCONFIG-2021
This class covers overly permissive cloud IAM trust policies that federate with GitHub's OIDC provider (token.actions.githubusercontent.com) but fail to constrain which workload may assume the role. The cloud role validates the OIDC token but checks only the audience claim (for example sts.amazonaws.com) while omitting the token.actions.githubusercontent.com:sub condition, or it uses a broad wildcard such as repo:org/* or a StringLike pattern instead of StringEquals, so any branch, any fork, or even an attacker-owned repository can mint a valid GitHub OIDC token and exchange it for cloud credentials. Because the sub claim encodes repository, branch, tag, and environment, dropping or loosening it removes the only binding between the role and the intended pipeline, yielding full assumption of the trusted role. Tinder Security Labs documented this in their AWS OIDC research, finding multiple real AWS roles assumable from unauthorized repositories due to missing subject validation, with the successful assumptions visible in CloudTrail. GitHub's OIDC support and the configure-aws-credentials path shipped in 2021, making this a long-standing systemic configuration risk.
- MEDIUMCVE-2026-96628
Arcane: Environment access token bypasses the admin-required authorization gate on `/api/debug/pprof`
- MEDIUMCVE-2026-58494
Wasmtime: WASI hard links bypass wasmtime-wasi's FilePerms for destination
- MEDIUMCVE-2026-102832
JupyterLab: Command link trust prompt in JupyterLab can trust a document other than the one clicked
- MEDIUMCVE-2026-103028
SFTPGo source-side file-pattern filter bypass during copy operations
- CRITICALCVE-2026-77459
Argo CD: AppProject destination-namespace and resource restrictions bypassed by PreDelete/PostDelete resource hooks