ApostropheCMS: Arbitrary file read via the attachment name in the import-export module
An account with the contributor role imports a crafted archive whose attachment metadata carries a traversal sequence and thereby reads host files with an allow-listed extension, which are then served at an unauthenticated /uploads URL.
Advisory ID: TP-2026-043
Product: ApostropheCMS (Node.js content management system; the vulnerability is in the official @apostrophecms/import-export module)
Vulnerability type: Path traversal to arbitrary file read (CWE-22)
CVE: CVE-2026-63667
CVSS 3.1: 6.5 (Medium) · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Vendor advisory: GHSA-79qf-vqgc-7xx3
Affected versions: @apostrophecms/import-export <= 3.6.1
Fixed in: @apostrophecms/import-export 3.6.2
Reported: 13 June 2026
Summary
ApostropheCMS is a Node.js content management system; the official @apostrophecms/import-export module imports and exports content as archives. During the gzip import the module rebuilds each attachment's on-disk source path from fields taken from the untrusted aposAttachments.json inside the uploaded archive, without checking those fields for traversal. The module's only ../ guard validates the tar entry names during extraction and leaves the JSON fields untouched, so a traversal sequence in name drives the path out of the extraction directory. The read file is copied into the public uploads directory and served there without authentication at a /uploads/attachments/... URL. An account with the contributor role thereby reads other users' files and host files with an allow-listed extension, while an extension allow-list prevents access to files such as .env or /etc/passwd. turingpoint verified the chain end to end and reported it to the vendor, who fixed it in @apostrophecms/import-export 3.6.2.
Root cause
The gzip import rebuilds each attachment's source path from the fields _id, name and extension of the uploaded aposAttachments.json through path.join (lib/formats/gzip.js:46). The module's only ../ guard checks with name.includes('../') only the tar entry names during extraction (lib/formats/gzip.js:143) and never inspects these JSON name/extension fields, so a name of ../../../../../../tmp/secret leaves the extraction directory. The path flows through insertAttachments (lib/methods/import.js:832) and attachment.insert (:1074) into uploadfs.copyIn into the public uploads directory served by express.static, and republishes the read file at an anonymous /uploads/attachments/... URL. The access requires an authenticated session (lib/methods/import.js:71), view on the target type (methods/index.js:54) and the upload-attachment permission (@apostrophecms/attachment/index.js:442), which the built-in contributor role holds, while guest and anonymous are rejected with 403. The extension allow-list throws before the read (@apostrophecms/attachment/index.js:411, getFileGroup), so only files whose real name ends in an allow-listed extension such as gif/jpg/png/svg/txt/pdf/csv are reachable, not .env or /etc/passwd.
Proof of Concept
# An account with the contributor role imports a crafted tar.gz.
# Inside the contained aposAttachments.json:
{ "_id": "evilatt0001",
"name": "../../../../../../tmp/apos_victim_secret",
"extension": "txt" }
# Afterwards readable anonymously:
GET /uploads/attachments/evilatt0001-apos_victim_secret.txt
path.join collapses the ../ sequence, so the host file /tmp/apos_victim_secret.txt is copied into the public uploads directory and served without authentication. turingpoint confirmed this live on Apostrophe 4.31.0 with @apostrophecms/import-export 3.6.1: the contributor import republished planted DB_PASSWORD/API_KEY values at HTTP 200, the guest import was rejected with forbidden, and a control extension env was blocked by the allow-list before the read.
Impact
- Read of host files whose name ends in an allow-listed extension (for example
.txt,.csv,.pdf), and of other users' documents. - Republishing of the read file at an unauthenticated
/uploads/attachments/...URL. - Reachable by an account with the contributor role; anonymous access is rejected.
- No whole-filesystem read and no code execution: the extension allow-list blocks
.env,.pem,.jsonand executable files before the read.
References
Is Something Like This in Your Software?
Our team found this vulnerability in the course of its work. Have your applications reviewed by the same specialists, with a penetration test from turingpoint.
