GHSA-79qf-vqgc-7xx3: Path Traversal

Published Sep 2, 2026
·
Updated

Summary

The @apostrophecms/import-export module reconstructs the on-disk source path of every imported attachment from JSON metadata contained in the uploaded archive.

The archive carries an aposAttachments.json file whose name and extension fields are concatenated into a filesystem path with no traversal check. The zip-slip guard that the module applies during tar extraction validates tar entry names only and does not cover this second path, which is built after extraction.

The file at the resulting path is read and copied into the public uploads directory, then served over HTTP without authentication. A ../ sequence in name makes the module read a file outside the extraction directory and publish it at an anonymous URL.

Result: an authenticated contributor reads any file on the host whose name ends in an allowlisted extension (other users' uploaded documents, text or CSV dumps, PDFs) by importing a crafted archive and fetching the planted attachment anonymously.

Affected

apostrophecms/apostrophe with the @apostrophecms/import-export module installed and registered. Module version 3.6.1 (current latest), tested against Apostrophe 4.31.0 (monorepo HEAD 4d478d9). Requires an account with the contributor role or higher; guest and anonymous requests are rejected. The module is not part of the default starter kit, so sites that never installed it are not affected. Files whose real name lacks an accepted file-group extension are not reachable.

Root cause

The import parser builds each attachment's source path by concatenating attacker-controlled JSON fields: lib/formats/gzip.js:46 sets file.path = path.join(attachmentFilesPath, ${attachment.id}-${attachment.name}.${attachment.extension}) from the aposAttachments.json entries in the uploaded archive. That path flows unchanged through lib/methods/import.js:832 (insertAttachments) into lib/methods/import.js:1074 (attachment.insert), where uploadfs copies the referenced file into the public uploads directory served by express.static. The archive's only traversal guard, lib/formats/gzip.js:143 (if (name.includes('../'))), validates tar entry names during extraction and never inspects the name/extension values used to construct the read path, so a name of ../../../../../../tmp/secret escapes attachmentFilesPath. Reaching the sink requires only an authenticated session (lib/methods/import.js:71), view permission on the target type (lib/methods/index.js:54), and the upload-attachment permission enforced at modules/@apostrophecms/attachment/index.js:442, which the built-in contributor role holds. The trailing .${extension} is appended and checked against the file-group allowlist in modules/@apostrophecms/attachment/index.js (getFileGroup), so the target file's real name must end in an accepted extension (txt, csv, pdf, xls, doc, svg, and similar).

Reproduction

Apostrophe 4.31.0 starter-kit-essentials, MongoDB, default roles, @apostrophecms/import-export 3.6.1 installed, local uploadfs backend.

1. Place a secret file outside the upload tree with an allowlisted extension.

$ cat /tmp/aposvictimsecret.txt TOP-SECRET DB DUMP DBPASSWORD=Pr0d-Secret-9981 APIKEY=sklivevictimabcdef

2. Build a gzip archive whose aposAttachments.json points the attachment name at that file through traversal (aposDocs.json is []).

[{"id":"evilatt0001","name":"../../../../../../../../../../../../tmp/aposvictimsecret","extension":"txt","title":"loot","docIds":[],"crops":[]}]

3. As a contributor, import the archive through the module's import action (POST /api/v1/@apostrophecms/<type>/import-export-import), then fetch the created attachment with no session.

$ curl -i http://localhost:3500/uploads/attachments/evilatt0001-apos-victim-secret.txt HTTP/1.1 200 OK Content-Type: text/plain; charset=UTF-8

TOP-SECRET DB DUMP DBPASSWORD=Pr0d-Secret-9981 APIKEY=sklivevictimabcdef

Live-verified: a contributor-driven import reads /tmp/aposvictimsecret.txt (outside the extraction directory) and serves it at an anonymous URL; the same import run as a guest is rejected at the upload-attachment check (modules/@apostrophecms/attachment/index.js:442).

Impact

- Read of arbitrary host files whose real name ends in an allowlisted extension (txt, csv, pdf, xls, doc, svg, and similar). - Disclosure of other users' uploaded documents and any allowlisted-extension file readable by the Node process. - The exfiltration target is copied to a public, unauthenticated URL. - Triggered by the contributor role in a single import, no admin interaction.

Credit

Jan Kahmen, turingpoint (jan@turingpoint.de)

Affected Software

1 affected componentFixes available
npm/@apostrophecms/import-export<=3.6.1
3.6.2

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/@apostrophecms/import-export to a version that resolves this vulnerability.

    Fixed in 3.6.2

Event History

Sep 2, 2026
Advisory Published
via GitHub·02:55 PM
Data Sourced
via GitHub·02:55 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed?

Apostrophe deployments are affected when the @apostrophecms/import-export module is installed and registered. The issue was reported for module version 3.6.1 and tested with Apostrophe 4.31.0.

2

What does an attacker need to exploit this?

The attacker needs authenticated contributor-level access and the ability to import a crafted archive containing aposAttachments.json metadata. No interaction from another user is required.

3

What data could be exposed?

An attacker can cause the server to read files outside the import extraction directory when their filenames end in an allowlisted extension. The copied file is placed in public uploads and can then be retrieved over HTTP without authentication; examples include other users' uploaded documents, text or CSV dumps, and PDFs.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203