# REPRO-2026-00352: Traefik digestAuth middleware gives empty secret to unknown usernames → auth bypass ## Summary Status: published Severity: high CVSS: Unknown CWE: CWE-287 (Improper Authentication) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00352 GHSA: GHSA-24QW-84Q9-39WJ CVE: CVE-2026-85595 ## Package Name: traefik/traefik Ecosystem: github Affected: All v1/v2/v3 releases shipping digestAuth: v2 <= v2.11.54 and v3.0.0 <= v3.7.10; v1 line, v2 minors < v2.11, v3 minors < v3.7 unmaintained and unpatched Fixed: Unknown ## Root Cause # RCA Report — Traefik digestAuth authentication bypass (GHSA-24qw-84q9-39wj) ## Summary Traefik's `digestAuth` middleware secret provider returns an **empty string** for a username that is absent from the configured htdigest user list, instead of signalling "no such user". The pinned `github.com/containous/go-http-auth` fork accepted that empty string as the user's `HA1` digest secret. Because every other input to the digest computation (`realm`, `nonce`, `opaque`, `uri`, `nc`, `cnonce`, `qop`, HTTP method) is either chosen by the client or handed to it in the `401` challenge, a remote **unauthenticated** attacker can forge a mathematically valid `Authorization: Digest` header for an **arbitrary unknown username** with `HA1 == ""` and reach any route protected by `digestAuth`. This was reproduced end-to-end against the real product (official Traefik Docker images) through the real HTTP endpoint: the forged request returned `HTTP 200` with the protected backend resource on vulnerable images and `401` on fixed images. ## Impact - **Package/component affected:** Traefik `pkg/middlewares/auth/digest_auth.go` (`secretDigest()`) combined with the pinned dependency `github.com/abbot/go-http-auth => github.com/containous/go-http-auth v0.4.1-0.20200324110947-a37a7636d23e` (`DigestAuth.CheckAuth()`). - **Affected versions (empirically verified this run):** `traefik:v2.11.54` (bypassed), `traefik:v3.6.11` (bypassed), and — contrary to the ticket's fixed-version claim — `traefik:v3.6.12` (**still bypassed**). Every release shipping the middleware without the fix is affected. - **Risk level / consequences:** Complete authentication bypass (high/critical). Any route protected only by `digestAuth` — including the Traefik dashboard/API when protected this way — is fully accessible without credentials. The forged username is also written to the access log's auth-user field, corrupting the audit trail. ## Impact Parity - **Disclosed/claimed maximum impact:** Authentication bypass / authz bypass on any digestAuth-protected route via a crafted digest response for an unknown username (`authz_bypass`, surface `api_remote`). - **Reproduced impact from this run:** Full parity for the claimed impact. A single remote HTTP request with a forged `Authorization: Digest` header for an unknown username (`attacker-unknown-user`) obtained `HTTP 200` and the protected backend body `PROTECTED-BACKEND-RESOURCE-OK` through the real Traefik HTTP endpoint on the vulnerable images; fixed images rejected the identical request with `401`. Valid credentials still work (`200`) and a wrong password is still rejected (`401`) on every image, proving the middleware was exercised normally and only the unknown-user path is broken. - **Parity:** `full` - **Not demonstrated:** Nothing further is claimed by the ticket. (No code execution is involved; the impact is authorization bypass, which was demonstrated in full.) ## Root Cause 1. **Traefik `pkg/middlewares/auth/digest_auth.go`** builds the authenticator with `goauth.NewDigestAuthenticator(realm, d.secretDigest)`. Its secret provider is: ```go func (d *digestAuth) secretDigest(user, realm string) string { if secret, ok := d.users[user+":"+realm]; ok { return secret } return "" // <-- unknown user gets an EMPTY secret instead of "reject" } ``` The users map is populated from the htdigest file (`user:realm:HA1` lines) by `getUsers`/`digestUserParser`. 2. **The pinned fork** `github.com/containous/go-http-auth` (replace directive in `go.mod`, pinned at `a37a7636d23e`) implements `DigestAuth.CheckAuth()` as: ```go HA1 := da.Secrets(auth["username"], da.Realm) // no check that HA1 == "" means "unknown user" HA2 := H(r.Method + ":" + auth["uri"]) KD := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], HA2}, ":")) if subtle.ConstantTimeCompare([]byte(KD), []byte(auth["response"])) != 1 { return "", nil } ``` With `HA1 == ""`, the attacker's expected response is `MD5(":" + nonce + ":" + nc + ":" + cnonce + ":auth:" + MD5("GET:" + uri))` — trivially computable from the `401` challenge (nonce, opaque) and the attacker's own choices. Traefik's caller then only checks `username == ""` to decide failure, so the forged unknown username authenticates successfully. 3. **Fix:** the fork commit `b975dcaa8c48` adds to `CheckAuth()`: ```go HA1 := da.Secrets(auth["username"], da.Realm) if HA1 == "" { return "", nil } ``` Traefik pulled the fixed fork in commit **`2116686308a2518bf1851a39eeec738f1e901195`** ("Bump github.com/containous/go-http-auth to b975dcaa8c48", go.mod replace directive `v0.4.1-0.20260804094822-b975dcaa8c48`). **Fixed-commit anchoring (verified via git, see `bundle/logs/git_source_verification.log`):** - Fix commit `21166863` is contained in tag `v2.11.55` (commit `1ac90fccb982f6de40f557493152bdb0c9f0a809`) — verified fixed at runtime. - Fix commit `21166863` is **absent from every `v3.6.x` tag** (checked `v3.6.0`–`v3.6.25`: all still pin fork `a37a7636d23e`). The first `v3` tag containing the fix is `v3.7.11` — verified fixed at runtime. - Consequently the ticket's "Fixed in ... v3.6.12" is **incorrect**: `traefik:v3.6.12` (tag commit `b782bd32d444af99d76e5f87970b02a9aa80ba97`, image digest `sha256:171c9c3565b29f6c133f1c1b43c5d4e5853415198e9e1078c001f8702ff66aec`) was empirically **still vulnerable** in this run. ## Reproduction Steps 1. Script: `bundle/repro/reproduction_steps.sh` (self-contained; creates all configs, the backend service, and the attacker client itself at runtime). 2. What it does: - Deploys the **real product** from official Docker images on a private Docker network: a `python:3-alpine` backend serving `PROTECTED-BACKEND-RESOURCE-OK`, and Traefik instances (static+dynamic file config) with a router `PathPrefix(`/protected`)` protected by the real `digestAuth` middleware (`usersFile` htdigest with one user `test` / realm `traefik` / password `secret`), plus an unprotected `/open` router for health checks. - Per image, runs **two clean attempts** (fresh container each): readiness wait, healthcheck (`/open` → 200, `/protected/` no-auth → 401), then the attacker sequence through the real HTTP endpoint: 1. `GET /protected/` with no credentials → capture `401` challenge (`realm`, `nonce`, `opaque`); 2. **Attack:** forged `Authorization: Digest` for unknown username `attacker-unknown-user` with `HA1 = ""`, `response = MD5(":"+nonce+":00000001:83cfda9a:auth:"+MD5("GET:/protected/"))`; 3. Sanity: known user with correct password (expect 200); 4. Sanity: known user with wrong password (expect 401). - Image matrix: `traefik:v2.11.54` (vulnerable primary), `traefik:v2.11.55` (fixed primary control), `traefik:v3.6.11` (vulnerable v3 line), `traefik:v3.6.12` (ticket-claimed fixed — tested empirically), `traefik:v3.7.11` (first actually-fixed v3 control). - Writes per-attempt request/response transcripts, a machine-readable `repro/exploit_results.json`, a supplementary git source verification log, and `repro/runtime_manifest.json`. 3. Expected evidence of reproduction (both consecutive runs produced exactly this): - `v2.11.54`, `v3.6.11`, **and `v3.6.12`**: attack step → `HTTP 200`, body `PROTECTED-BACKEND-RESOURCE-OK` (authentication **bypassed**, request forwarded to the protected backend). - `v2.11.55` and `v3.7.11`: attack step → `HTTP 401` with a fresh digest challenge (rejected). - Sanity on every image: correct credentials → 200, wrong password → 401. ## Evidence - Per-attempt HTTP transcripts and JSON results: `bundle/repro/artifacts/http/{vuln_primary,fixed_primary,vuln_v3,claimed_fixed_v3,fixed_v3_control}_v*._attempt{1,2}.{txt,json}` - Aggregated verdict: `bundle/repro/exploit_results.json` - Runtime manifest (image digests, target identity, artifact hashes): `bundle/repro/runtime_manifest.json` - Git source verification (fix-commit ancestry for all five tags, fork diff): `bundle/logs/git_source_verification.log` - Full run log: `bundle/logs/reproduction_steps.log` - Key excerpt (`vuln_primary_vv2.11.54_attempt1.txt`): ``` === step2 ATTACK forged digest, unknown username, empty HA1 secret === > Authorization: Digest username="attacker-unknown-user", realm="traefik", nonce="4jleqQagi0BjT/09", uri="/protected/", algorithm=MD5, qop=auth, nc=00000001, cnonce="83cfda9a", response="939d544cfabce8f635f5c4bd372dc89c", opaque="YF7w7S9FYLwAFyIV" < HTTP/1.1 200 < body: 'PROTECTED-BACKEND-RESOURCE-OK\n' ``` and the fixed control (`fixed_primary_vv2.11.55_attempt1.txt`): identical forged header → `< HTTP/1.1 401` + fresh `Www-Authenticate` challenge. - Environment: Docker (rootless daemon, Alpine), linux/amd64; images: `traefik:v2.11.54` `sha256:f10edd30…`, `traefik:v2.11.55` `sha256:4f87b6b3…`, `traefik:v3.6.11` `sha256:acfc8065…`, `traefik:v3.6.12` `sha256:171c9c35…`, `traefik:v3.7.11` `sha256:5203c3f3…`, `python:3-alpine` backend. ## Recommendations / Next Steps - **Fix (already upstream):** keep `containous/go-http-auth >= b975dcaa8c48` (reject `HA1 == ""` in `CheckAuth`). Defence-in-depth: make Traefik's `secretDigest()` distinguish "no such user" from a secret (return an error / ok flag) rather than returning `""`, and reject before `CheckAuth` consumes it. - **Upgrade guidance:** upgrade the v2 line to **v2.11.55 or later**; for the v3 line the ticket's "v3.6.12" is **not** fixed — upgrade to **v3.7.11 or later** (or any build whose `go.mod` pins the fork at `b975dcaa8c48`). Operators on v3.6.x should treat digestAuth-protected routes as unprotected until they upgrade. - **Detection:** access-log lines whose auth-user is not a configured user on a digestAuth-protected route indicate exploitation. - **Testing recommendation:** add a regression test that a forged digest for a username absent from the htdigest file is rejected (the fork's `digest_test.go` gained exactly such a case in `b975dcaa8c48`). ## Additional Notes - **Idempotency:** the script was executed twice consecutively; both runs exited 0 with identical results (10/10 attempts as expected, `CONFIRMED: True`). It cleans up all containers/networks it creates (start and via `trap ... EXIT`). - **Ticket discrepancy (important for the Judge):** the ticket/GHSA text says "Fixed in v2.11.55/v3.6.12". The v2.11.55 part is confirmed; the v3.6.12 part is **disproved** both by git ancestry (fix commit `21166863` is in no `v3.6.x` tag) and by runtime evidence (bypass succeeds against the official `traefik:v3.6.12` image). The first fixed v3 release is v3.7.11. - **Limitations:** the reproduction uses Traefik's default realm `traefik` and the default MD5/qop=auth parameters; the vulnerability is independent of those choices (the secret lookup key and the digest formula adapt to whatever the server advertises). `removeHeader: true` was enabled on the middleware; the bypass works regardless. No sanitizers were used (product-mode proof, `sanitizer_used=false`). ## Reproduction Details Reproduced: 2026-09-11T10:13:47.020Z Duration: 3941 seconds Tool calls: 174 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00352 pruva-verify GHSA-24QW-84Q9-39WJ pruva-verify CVE-2026-85595 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00352&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00352/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/traefik/traefik/security/advisories/GHSA-24qw-84q9-39wj - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-85595 - Source: https://github.com/traefik/traefik/security/advisories/GHSA-24qw-84q9-39wj ## Artifacts - bundle/repro/rca_report.md (analysis, 11077 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 25957 bytes) - bundle/logs/git_source_verification.log (log, 1661 bytes) - bundle/repro/artifacts/http/claimed_fixed_v3_vv3.6.12_attempt1.json (other, 517 bytes) - bundle/repro/artifacts/http/claimed_fixed_v3_vv3.6.12_attempt1.txt (other, 1912 bytes) - bundle/repro/artifacts/http/claimed_fixed_v3_vv3.6.12_attempt2.json (other, 517 bytes) - bundle/repro/artifacts/http/claimed_fixed_v3_vv3.6.12_attempt2.txt (other, 1912 bytes) - bundle/repro/artifacts/http/fixed_primary_vv2.11.55_attempt1.json (other, 502 bytes) - bundle/repro/artifacts/http/fixed_primary_vv2.11.55_attempt1.txt (other, 1987 bytes) - bundle/repro/artifacts/http/fixed_primary_vv2.11.55_attempt2.json (other, 502 bytes) - bundle/repro/artifacts/http/fixed_primary_vv2.11.55_attempt2.txt (other, 1987 bytes) - bundle/repro/artifacts/http/fixed_v3_control_vv3.7.11_attempt1.json (other, 504 bytes) - bundle/repro/artifacts/http/fixed_v3_control_vv3.7.11_attempt1.txt (other, 1987 bytes) - bundle/repro/artifacts/http/fixed_v3_control_vv3.7.11_attempt2.json (other, 504 bytes) - bundle/repro/artifacts/http/fixed_v3_control_vv3.7.11_attempt2.txt (other, 1987 bytes) - bundle/repro/artifacts/http/vuln_primary_vv2.11.54_attempt1.json (other, 514 bytes) - bundle/repro/artifacts/http/vuln_primary_vv2.11.54_attempt1.txt (other, 1912 bytes) - bundle/repro/artifacts/http/vuln_primary_vv2.11.54_attempt2.json (other, 514 bytes) - bundle/repro/artifacts/http/vuln_primary_vv2.11.54_attempt2.txt (other, 1912 bytes) - bundle/repro/artifacts/http/vuln_v3_vv3.6.11_attempt1.json (other, 508 bytes) - bundle/repro/artifacts/http/vuln_v3_vv3.6.11_attempt1.txt (other, 1912 bytes) - bundle/repro/artifacts/http/vuln_v3_vv3.6.11_attempt2.json (other, 508 bytes) - bundle/repro/artifacts/http/vuln_v3_vv3.6.11_attempt2.txt (other, 1912 bytes) - bundle/repro/exploit_results.json (other, 2691 bytes) - bundle/repro/runtime_manifest.json (other, 6944 bytes) - bundle/repro/validation_verdict.json (other, 1500 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00352 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00352/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00352 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev