# Distribution: stale blob access resurrection via repo-scoped redis descriptor cache invalidation

Distribution registry with redis blob descriptor cache leaves stale repo-scoped membership after delete, so a peer repo repopulating the shared descriptor restores read access to a deleted blob.

- Payout: Research
- Severity: high
- Program: Distribution
- Found by: 1seal
- Disclosed: 2026-04-06
- CVE: CVE-2026-35172
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-f2g3-hh2r-cwgc
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-f2g3-hh2r-cwgc
- Tags: area:cloud domain:cloud domain:infra impact:data-leak kind:research platform:ghsa sev:high tech:go tech:redis technique:incomplete-fix technique:source-review vuln:access-control vuln:info-disclosure

**Target:** Registry blob descriptor cache (redis backend) / blob delete and stat endpoints
**Root cause:** repositoryScopedRedisBlobDescriptorService.Clear only calls upstream.Clear, deleting the shared digest descriptor but never removing the digest from the repo-scoped membership set; Stat later trusts stale membership plus a peer-repopulated shared descriptor.
**Steps:**
1. Enable `storage.cache.blobdescriptor: redis` and `storage.delete.enabled: true`; push same digest to `repo a` and `repo b`.
2. Delete the blob link from `repo a`; confirm `repo a` returns `blob unknown`.
3. Request the digest from `repo b` (still linked) to repopulate the shared descriptor.
4. Request the digest from `repo a` again; it now succeeds via stale repo-a membership.
**How found:** Code reading of the split invalidation path: traced linkedBlobStore.Delete -> cachedBlobStatter.Clear -> repositoryScopedRedisBlobDescriptorService.Clear and noticed membership check without membership removal, then hypothesized peer-repo warming would restore access; validated with a miniredis integration harness.
**Exploitation:** Any anonymous/unprivileged reader of `repo a` regains access to content the operator explicitly deleted, once any peer repository holding the same digest warms the shared descriptor. No write access needed after the delete.
**Insight:** When a cache layer splits shared-digest state from per-repo membership state, audit every Clear/Delete path for whether it removes both; a membership set that is only checked, never pruned, lets any peer that re-warms the shared key silently re-authorize the deleted repo.

