Mar 1, 2026·5 min read·10 visits
CVE-2024-39897 allows authenticated attackers to read private container layers by abusing the OCI 'mount' API. Zot's deduplication logic failed to verify source permissions, allowing cross-repository mounting of blobs via their digest. Fixed in version 2.1.0.
Zot, an OCI-compliant container image registry, contains an authorization bypass vulnerability in its storage deduplication logic. The issue allows authenticated users to mount (copy) blobs from repositories they do not have permission to access, provided they know the SHA256 digest of the target blob. This effectively grants unauthorized read access to private container layers.
CVE-2024-39897 is a logical access control flaw within the Zot container registry, specifically affecting the implementation of the OCI distribution specification's cross-repository blob mounting feature. In standard OCI operations, a client pushing an image can request to 'mount' a layer (blob) from another repository to avoid re-uploading duplicate data. This optimization relies on the registry verifying that the client has read access to the source repository before allowing the mount.
In affected versions of Zot (prior to 2.1.0), this access check was missing or insufficient. The system performed a global lookup for the requested blob digest in its deduplication cache. If the blob existed anywhere in the storage backend, Zot permitted the mount operation into the attacker's repository. This vulnerability is classified as an Authorization Bypass Through User-Controlled Key (CWE-639), as the blob digest serves as the key to access data without proper authorization context.
The root cause lies in the dissociation between storage deduplication logic and access control enforcement in pkg/api/routes.go. Zot utilizes a global cache (often backed by BoltDB or DynamoDB) to track blobs across all repositories to save space. When a POST request is made to /v2/<repo>/blobs/uploads/?mount=<digest>, the handler invokes imgStore.CheckBlob(name, digest).
In the vulnerable code path, CheckBlob verified the existence of the digest in the global cache. Upon a cache hit, the system assumed the blob was available for mounting and proceeded to link it to the destination repository. The critical failure was the absence of an intervening check to validate the user's permissions against the source repository (or any repository strictly associated with that blob). The system implicitly trusted that knowledge of the digest equated to authorization to access the content, violating the principle of least privilege.
The vulnerability manifests in the handling of the mount query parameter during blob uploads. Below is a conceptual reconstruction of the flaw based on the patch analysis.
Vulnerable Logic (Simplified):
In the CreateBlobUpload handler, the code checks if the blob exists globally:
// BEFORE FIX: Global lookup without permission check
if mountDigest != "" {
// Checks global storage for digest existence
exists, err := imgStore.CheckBlob(mountDigest)
if err == nil && exists {
// Vulnerability: Directly mounts the blob to the user's repo
// No verification that user has Read access to the origin of the blob
err = imgStore.MountBlob(destinationRepo, mountDigest)
return 201, "Created"
}
}Patched Logic: The fix introduces a mandatory authorization step. The system must now identify a repository where the user does have read access that also contains the requested blob. If no such repository is found, the mount is denied, forcing the user to upload the data themselves.
// AFTER FIX: explicit permission validation
if mountDigest != "" {
// 1. Verify existence
exists, _ := imgStore.CheckBlob(mountDigest)
// 2. NEW: Verify authorization via helper
// 'canMount' checks if the user has READ access to at least
// one repository that actually holds this blob.
if exists && c.canMount(user, mountDigest) {
imgStore.MountBlob(destinationRepo, mountDigest)
return 201, "Created"
}
// If check fails, fall back to standard upload (202 Accepted)
return 202, "Accepted"
}Exploitation requires an authenticated account and knowledge of a target blob's SHA256 digest. The attacker does not need to guess the digest; they might obtain it from leaked manifests, public metadata, or side-channel information.
Attack Scenario:
secret-project/app:latest and obtains the digest of a configuration layer (sha256:deadbeef...).attacker/repo requesting to mount the target digest.
POST /v2/attacker/repo/blobs/uploads/?mount=sha256:deadbeef...&from=secret-project/app HTTP/1.1
Host: zot-registry.local
Authorization: Bearer <Attacker_Token>sha256:deadbeef... in its global storage. Ignoring the ACLs on secret-project/app, it links the blob to attacker/repo.GET /v2/attacker/repo/blobs/sha256:deadbeef... request to download the layer content.The primary impact is Confidentiality Loss. The vulnerability allows the leakage of proprietary code, configuration files, binary artifacts, and potential secrets (API keys, certificates) embedded within container layers.
Risk Factors:
The vulnerability is addressed in Zot version 2.1.0. Administrators should upgrade immediately to ensure canMount checks are enforced.
Immediate Mitigation (Configuration): If an upgrade is not feasible, administrators can disable storage deduplication. This prevents the code path that performs the insecure global lookup, though it will increase storage consumption.
{
"storage": {
"dedupe": false
}
}Verification:
After patching, verify the fix using the TestAuthorizationMountBlob logic: attempt to mount a blob from a repository the test user cannot access. The server should respond with 202 Accepted (indicating the mount failed and a fresh upload is expected) rather than 201 Created.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
zot project-zot | < 2.1.0 | 2.1.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-639 |
| Attack Vector | Network |
| CVSS Score | 4.3 (Medium) |
| EPSS Score | 0.36% |
| Impact | Confidentiality Loss |
| Exploit Status | PoC Available |