CVE-2026-85595: Verified Reproduction
CVE-2026-85595: Traefik digestAuth middleware gives empty secret to unknown usernames → auth bypass
CVE-2026-85595 is verified against traefik/traefik · github. Affected versions: 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. Vulnerability class: Auth Bypass. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00352.
What Is CVE-2026-85595?
CVE-2026-85595 is a high-severity Auth Bypass vulnerability affecting traefik/traefik 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. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00352).
CVE-2026-85595 Severity
CVE-2026-85595 is rated high severity.
High — serious impact or readily exploitable. Prioritize remediation.
Affected traefik/traefik Versions
traefik/traefik · github versions 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 are affected.
How to Reproduce CVE-2026-85595
pruva-verify REPRO-2026-00352 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00352/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-85595
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
forged Authorization: Digest header for an unknown username (attacker-unknown-user) computed with the empty HA1 secret the vulnerable secretDigest() returns; response = MD5(":"+nonce+":00000001:83cfda9a:auth:"+MD5("GET:/protected/"))
- HTTP GET /protected/ on a digestAuth-protected Traefik router
- pkg/middlewares/auth/digest_auth.go secretDigest() returns "" for unknown user
- pinned containous/go-http-auth fork CheckAuth() accepts empty HA1
- request forwarded to protected backend (HTTP 200)
How the agent worked
Root Cause and Exploit Chain for CVE-2026-85595
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.
- Package/component affected: Traefik
pkg/middlewares/auth/digest_auth.go(secretDigest()) combined with the pinned dependencygithub.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, surfaceapi_remote). - Reproduced impact from this run: Full parity for the claimed impact. A single remote HTTP request with a forged
Authorization: Digestheader for an unknown username (attacker-unknown-user) obtainedHTTP 200and the protected backend bodyPROTECTED-BACKEND-RESOURCE-OKthrough the real Traefik HTTP endpoint on the vulnerable images; fixed images rejected the identical request with401. 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
- Traefik
pkg/middlewares/auth/digest_auth.gobuilds the authenticator withgoauth.NewDigestAuthenticator(realm, d.secretDigest). Its secret provider is:
The users map is populated from the htdigest file (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" }user:realm:HA1lines) bygetUsers/digestUserParser. - The pinned fork
github.com/containous/go-http-auth(replace directive ingo.mod, pinned ata37a7636d23e) implementsDigestAuth.CheckAuth()as:
WithHA1 := 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 }HA1 == "", the attacker's expected response isMD5(":" + nonce + ":" + nc + ":" + cnonce + ":auth:" + MD5("GET:" + uri))— trivially computable from the401challenge (nonce, opaque) and the attacker's own choices. Traefik's caller then only checksusername == ""to decide failure, so the forged unknown username authenticates successfully. - Fix: the fork commit
b975dcaa8c48adds toCheckAuth():
Traefik pulled the fixed fork in commitHA1 := da.Secrets(auth["username"], da.Realm) if HA1 == "" { return "", nil }2116686308a2518bf1851a39eeec738f1e901195("Bump github.com/containous/go-http-auth to b975dcaa8c48", go.mod replace directivev0.4.1-0.20260804094822-b975dcaa8c48).
Fixed-commit anchoring (verified via git, see bundle/logs/git_source_verification.log):
- Fix commit
21166863is contained in tagv2.11.55(commit1ac90fccb982f6de40f557493152bdb0c9f0a809) — verified fixed at runtime. - Fix commit
21166863is absent from everyv3.6.xtag (checkedv3.6.0–v3.6.25: all still pin forka37a7636d23e). The firstv3tag containing the fix isv3.7.11— verified fixed at runtime. - Consequently the ticket's "Fixed in ... v3.6.12" is incorrect:
traefik:v3.6.12(tag commitb782bd32d444af99d76e5f87970b02a9aa80ba97, image digestsha256:171c9c3565b29f6c133f1c1b43c5d4e5853415198e9e1078c001f8702ff66aec) was empirically still vulnerable in this run.
Reproduction Steps
- Script:
bundle/repro/reproduction_steps.sh(self-contained; creates all configs, the backend service, and the attacker client itself at runtime). - What it does:
- Deploys the real product from official Docker images on a private Docker network: a
python:3-alpinebackend servingPROTECTED-BACKEND-RESOURCE-OK, and Traefik instances (static+dynamic file config) with a routerPathPrefix(/protected)protected by the realdigestAuthmiddleware (usersFilehtdigest with one usertest/ realmtraefik/ passwordsecret), plus an unprotected/openrouter 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:GET /protected/with no credentials → capture401challenge (realm,nonce,opaque);- Attack: forged
Authorization: Digestfor unknown usernameattacker-unknown-userwithHA1 = "",response = MD5(":"+nonce+":00000001:83cfda9a:auth:"+MD5("GET:/protected/")); - Sanity: known user with correct password (expect 200);
- 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, andrepro/runtime_manifest.json.
- Deploys the real product from official Docker images on a private Docker network: a
- Expected evidence of reproduction (both consecutive runs produced exactly this):
v2.11.54,v3.6.11, andv3.6.12: attack step →HTTP 200, bodyPROTECTED-BACKEND-RESOURCE-OK(authentication bypassed, request forwarded to the protected backend).v2.11.55andv3.7.11: attack step →HTTP 401with 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):
and the fixed control (=== 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'fixed_primary_vv2.11.55_attempt1.txt): identical forged header →< HTTP/1.1 401+ freshWww-Authenticatechallenge. - Environment: Docker (rootless daemon, Alpine), linux/amd64; images:
traefik:v2.11.54sha256:f10edd30…,traefik:v2.11.55sha256:4f87b6b3…,traefik:v3.6.11sha256:acfc8065…,traefik:v3.6.12sha256:171c9c35…,traefik:v3.7.11sha256:5203c3f3…,python:3-alpinebackend.
Recommendations / Next Steps
- Fix (already upstream): keep
containous/go-http-auth >= b975dcaa8c48(rejectHA1 == ""inCheckAuth). Defence-in-depth: make Traefik'ssecretDigest()distinguish "no such user" from a secret (return an error / ok flag) rather than returning"", and reject beforeCheckAuthconsumes 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.modpins the fork atb975dcaa8c48). 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.gogained exactly such a case inb975dcaa8c48).
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 viatrap ... 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
21166863is in nov3.6.xtag) and by runtime evidence (bypass succeeds against the officialtraefik:v3.6.12image). The first fixed v3 release is v3.7.11. - Limitations: the reproduction uses Traefik's default realm
traefikand 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: truewas enabled on the middleware; the bypass works regardless. No sanitizers were used (product-mode proof,sanitizer_used=false).
CVE-2026-85595 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
echo "=== go ==="; (go version || true); echo "=== docker ==="; (docker --version || true); echo "=== node/python ==="; node --version; python3 --version; echo "=== project cache ==="; ls -la /pruva/project-cache/ 2>/dev/null || echo "no project cache dir"; ls -la /pruva/project-cache/repo-mirrors/ 2>/dev/null; cat /pruva/project-cache/.pruva/cache_manifest.json 2>/dev/null || echo "no cache manifest"=== go === === docker === Docker version 29.1.3, build 29.1.3-0ubuntu4.1 === node/python === v24.18.0 Python 3.14.4 === project cache === total 4
ls -la /workspace/bundle/repro/ /workspace/bundle/logs/repro/ /workspace/bundle/support/ 2>/dev/null; cat /workspace/bundle/artifact_promotion_manifest.json 2>/dev/null{"cwd":"/workspace","duration_ms":49,"exit_code":0,"stderr":"","stdout":"/workspace/bundle/logs/repro/:\ntotal 0\ndrwxr-xr-x 2 vscode vscode 40 Sep 5 07:05 .\ndrwxr-xr-x 5 vscode vscode 100 Sep 5 07:05 ..\n\n/workspace/bundle/repro/:\ntotal 0\ndrwxr-xr-x 2 vscode vscode 40 Sep 5 07:05 .\ndrwx------ 8 vscode vscode 260 Sep 5 07:05 ..\n\n/workspace/bundle/support/:\ntotal 0\ndrwxr-xr-x 2 vscode vscode 40 Sep 5 07:04 .\ndrwx------ 8 vscode vscode 260 Sep 5 07:05 ..\n{\n \"schema_version\": 1,\n \"run_id\": \"a26c6a1b-cb69-4bb8-ac4b-01fb1d6efa6f\",\n \"updated_at\": \"2026-09-05T07:05:39.269174549Z\",\n \"entries\": [\n {\n \"logical_path\": \"claim_contract/report.json\",\n \"stage\": \"claim_contract\",\n \"role\": \"claim_contract_report\",\n \"destination\": \"private_only\",\n \"required_for_verdict\": true,\n \"publishable\": false,\n \"reason\": \"Structural Claim Contract gate report; private orchestration data\",\n \"sha256\": \"c28f98d6e6fee241cc198e91426b183e899583f8142cb25cb8a1160493e5e236\",\n \"size_bytes\": 537,\n \"content_type\": \"application/json\",\n \"declared_by\": \"stage_validator\"\n },\n… [truncated]cd /pruva/project-cache/repo-mirrors && git clone --filter=blob:none --no-checkout https://github.com/traefik/traefik.git traefik.git 2>&1 | tail -5Cloning into 'traefik.git'...
cd /pruva/project-cache/repo-mirrors/traefik.git && git fetch --tags --force origin 'refs/tags/v3.6.*:refs/tags/v3.6.*' 'refs/tags/v2.11.*:refs/tags/v2.11.*' 2>&1 | tail -3; git rev-parse v3.6.11 v3.6.12 v2.11.54 v2.11.55 2>&133219a0af86c41a8db81d37c444f65172bfb3e35 b782bd32d444af99d76e5f87970b02a9aa80ba97 1e8e8c200cce5fbd1fff8579e5e9dc0311a8a54a 1ac90fccb982f6de40f557493152bdb0c9f0a809
cd /pruva/project-cache/repo-mirrors/traefik.git && git diff v3.6.11..v3.6.12 --stat -- pkg/middlewares/auth/ go.mod | head -30; echo ===FULLDIFF===; git diff v3.6.11..v3.6.12 -- pkg/middlewares/auth/digest.gogo.mod | 4 ++-- pkg/middlewares/auth/basic_auth.go | 2 ++ pkg/middlewares/auth/basic_auth_test.go | 24 ++++++++++++++++++++++++ pkg/middlewares/auth/digest_auth.go | 2 ++ pkg/middlewares/auth/digest_auth_test.go | 27 +++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 2 deletions(-) ===FULLDIFF===
cd /pruva/project-cache/repo-mirrors/traefik.git && git diff v3.6.11..v3.6.12 -- go.mod pkg/middlewares/auth/digest_auth.go pkg/middlewares/auth/basic_auth.go{"cwd":"/workspace","duration_ms":50,"exit_code":0,"stderr":"","stdout":"diff --git a/go.mod b/go.mod\nindex c540bd8e..becc90a9 100644\n--- a/go.mod\n+++ b/go.mod\n@@ -23,7 +23,7 @@ require (\n \tgithub.com/docker/go-connections v0.6.0\n \tgithub.com/fatih/structs v1.1.0\n \tgithub.com/fsnotify/fsnotify v1.9.0\n-\tgithub.com/go-acme/lego/v4 v4.32.0\n+\tgithub.com/go-acme/lego/v4 v4.33.0\n \tgithub.com/go-kit/kit v0.13.0\n \tgithub.com/go-kit/log v0.2.1\n \tgithub.com/golang/protobuf v1.5.4\n@@ -102,7 +102,7 @@ require (\n \tgolang.org/x/text v0.34.0\n \tgolang.org/x/time v0.14.0\n \tgolang.org/x/tools v0.41.0\n-\tgoogle.golang.org/grpc v1.79.1\n+\tgoogle.golang.org/grpc v1.79.3\n \tgopkg.in/natefinch/lumberjack.v2 v2.2.1\n \tgopkg.in/yaml.v3 v3.0.1\n \tk8s.io/api v0.34.3\ndiff --git a/pkg/middlewares/auth/basic_auth.go b/pkg/middlewares/auth/basic_auth.go\nindex 18d7995e..a6b99b45 100644\n--- a/pkg/middlewares/auth/basic_auth.go\n+++ b/pkg/middlewares/auth/basic_auth.go\n@@ -99,6 +99,8 @@ func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {\n \treq.URL.User = url.User(user)\n \n \tif b.headerField != \"\" {\n+\t\t// TODO Deprecated we should add the header wit… [truncated]Artifacts and Evidence for CVE-2026-85595
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-85595
FAQ: CVE-2026-85595
Is CVE-2026-85595 exploitable?
How severe is CVE-2026-85595?
What type of vulnerability is CVE-2026-85595?
Which versions of traefik/traefik are affected by CVE-2026-85595?
How can I reproduce CVE-2026-85595?
Is the CVE-2026-85595 reproduction verified?
References for CVE-2026-85595
Authoritative sources for CVE-2026-85595 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.