CVE-2026-112953
ApostropheCMS: Exposure of Sensitive Information via predictable export filenames
ApostropheCMS is an open-source Node.js content management system. Prior to 3.7.1 of the import-export module, predictable export archive filenames allow an unauthenticated requester to download another user's export from publicly readable export storage before the archive expires or is removed. A user with export permission must first create the archive, and sites with exports disabled, private upload storage, or authenticated upload delivery are not exposed through this network-download path. This issue is fixed in version 3.7.1.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Passive
What an attacker can affect
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
6.0
ApostropheCMS: Exposure of Sensitive Information via predictable export filenames
ApostropheCMS is an open-source Node.js content management system. Prior to 3.7.1 of the import-export module, predictable export archive filenames allow an unauthenticated requester to download another user's export from publicly readable export storage before the archive expires or is removed. A user with export permission must first create the archive, and sites with exports disabled, private upload storage, or authenticated upload delivery are not exposed through this network-download path. This issue is fixed in version 3.7.1.
- Record
CVE-2026-112953- CVSS 4.0
- 6.0 Medium
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-200
- Updated
- 2026-11-02
EUVDEUVD-2026-105103
6.0
ApostropheCMS is an open-source Node.js content management system. Prior to 3.7.1 of the import-export module, predictable export archive filenames allow an unauthenticated requester to download another user's export from publicly readable export storage before the archive expires or is removed. A user with export permission must first create the archive, and sites with exports disabled, private upload storage, or authenticated upload delivery are not exposed through this network-download path. This issue is fixed in version 3.7.1.
- Record
EUVD-2026-105103- CVSS 4.0
- 6.0 Medium
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-5gc8-frw3-3vqm
6.0
ApostropheCMS: Exposure of Sensitive Information via predictable export filenames
Summary
When a user exports content through @apostrophecms/import-export, the module copies the generated archive into uploadfs at /exports/ and returns a public URL under /uploads/exports/. The filename is fully deterministic: site shortName, document type, a YYYYMMDDHHmmss timestamp, and the format extension. There is no secret token.
Anyone who can guess that path can download the archive with a normal unauthenticated HTTP GET while the file is still present (10 minutes by default, longer if the site raises importExport.export.expiration). That is a different model from attachment URLs, which rely on hard-to-guess IDs.
Sites that disable export, store uploads on a private backend with no anonymous read, or serve /uploads only behind authentication are not hit by the network download step—but the predictable naming is still a design flaw for the default local/public uploadfs layout.
Import-only workflows and imports themselves are out of scope; this is specific to the export download URL.
Affected configurations
All of the following must apply:
- The
@apostrophecms/import-exportmodule is enabled and export is allowed for at least one page or piece type, and - Export files are served the usual way:
copyInto/exports/…and delivery via the publicuploadsURL (default local uploadfs or any deployment that exposes the same path without per-file auth).
If export is turned off per type or globally, or /uploads is not world-readable, an anonymous attacker cannot pull the file over HTTP—but a co-resident attacker with filesystem or bucket listing access would still see guessable names.
Remediation
- Put a high-entropy component in the export filename (e.g.
apos.util.generateId()), or - Stop publishing exports on a world-readable URL—serve downloads through an authenticated route and keep objects in a private prefix until then, or
- Document that
importExport.export.expirationmust stay short and treat/uploads/exports/as sensitive (weak on its own).
Details
Technical details
exportFile() in packages/import-export/lib/methods/export.js builds the name and publishes it:
const date = dayjs().format('YYYYMMDDHHmmss');
const filename = `${self.apos.shortName}-${req.body.type.toLowerCase()}-export-${date}${format.extension}`;
const downloadPath = path.join('/exports', filename);
await copyIn(filepath, downloadPath);
The admin UI sends type as the module name (e.g. article), which matches the filename. The download URL is only shown in the exporter’s notification; nothing binds the HTTP request to that user afterward. After expiration (default 1000 * 60 * 10 ms) a timer deletes the object from uploadfs, which limits exposure but leaves a brute-forceable window.
Proof of concept
The script boots a minimal Apostrophe app with import-export, performs an export as the task user, then downloads the same archive without a session—first by path guessing, then with raw fetch. It exits non-zero on failure.
Requirements: Node 18+, pnpm install at the monorepo root (so apostrophe and @apostrophecms/import-export resolve). Set APOSTROPHE_ROOT to the checkout path.
#!/usr/bin/env bash
set -euo pipefail
# --- config ---
APOSTROPHE_ROOT="${APOSTROPHE_ROOT:-}"
if [[ -z "${APOSTROPHE_ROOT}" || ! -f "${APOSTROPHE_ROOT}/packages/import-export/package.json" ]]; then
echo "Set APOSTROPHE_ROOT to your apostrophe monorepo checkout." >&2
exit 1
fi
export APOS_TEST_DB_PROTOCOL=sqlite
cd "${APOSTROPHE_ROOT}/packages/import-export"
# --- run embedded PoC (no extra repo files) ---
node <<'NODE'
const assert = require('node:assert/strict');
const path = require('node:path');
const dayjs = require('dayjs');
const t = require('apostrophe/test-lib/util.js');
(async () => {
let apos;
const shortName = 'poc-export-site';
const exportsPath = (root) => path.join(root, 'public/uploads/exports');
try {
apos…
- Record
GHSA-5gc8-frw3-3vqm- CVSS 4.0
- 6.0 Medium
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-200
- Updated
- 2026-11-02
NVD
6.0
ApostropheCMS is an open-source Node.js content management system. Prior to 3.7.1 of the import-export module, predictable export archive filenames allow an unauthenticated requester to download another user's export from publicly readable export storage before the archive expires or is removed. A user with export permission must first create the archive, and sites with exports disabled, private upload storage, or authenticated upload delivery are not exposed through this network-download path. This issue is fixed in version 3.7.1.
- Record
CVE-2026-112953- CVSS 4.0
- 6.0 Medium
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-200
- Updated
- 2026-11-02
OSV
6.0
ApostropheCMS: Exposure of Sensitive Information via predictable export filenames
ApostropheCMS is an open-source Node.js content management system. Prior to 3.7.1 of the import-export module, predictable export archive filenames allow an unauthenticated requester to download another user's export from publicly readable export storage before the archive expires or is removed. A user with export permission must first create the archive, and sites with exports disabled, private upload storage, or authenticated upload delivery are not exposed through this network-download path. This issue is fixed in version 3.7.1.
- Record
CVE-2026-112953- CVSS 4.0
- 6.0 Medium
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-200
- Updated
- 2026-11-02
OSVGHSA-5gc8-frw3-3vqm
6.0
ApostropheCMS: Exposure of Sensitive Information via predictable export filenames
Summary
When a user exports content through @apostrophecms/import-export, the module copies the generated archive into uploadfs at /exports/ and returns a public URL under /uploads/exports/. The filename is fully deterministic: site shortName, document type, a YYYYMMDDHHmmss timestamp, and the format extension. There is no secret token.
Anyone who can guess that path can download the archive with a normal unauthenticated HTTP GET while the file is still present (10 minutes by default, longer if the site raises importExport.export.expiration). That is a different model from attachment URLs, which rely on hard-to-guess IDs.
Sites that disable export, store uploads on a private backend with no anonymous read, or serve /uploads only behind authentication are not hit by the network download step—but the predictable naming is still a design flaw for the default local/public uploadfs layout.
Import-only workflows and imports themselves are out of scope; this is specific to the export download URL.
Affected configurations
All of the following must apply:
- The
@apostrophecms/import-exportmodule is enabled and export is allowed for at least one page or piece type, and - Export files are served the usual way:
copyInto/exports/…and delivery via the publicuploadsURL (default local uploadfs or any deployment that exposes the same path without per-file auth).
If export is turned off per type or globally, or /uploads is not world-readable, an anonymous attacker cannot pull the file over HTTP—but a co-resident attacker with filesystem or bucket listing access would still see guessable names.
Remediation
- Put a high-entropy component in the export filename (e.g.
apos.util.generateId()), or - Stop publishing exports on a world-readable URL—serve downloads through an authenticated route and keep objects in a private prefix until then, or
- Document that
importExport.export.expirationmust stay short and treat/uploads/exports/as sensitive (weak on its own).
Details
Technical details
exportFile() in packages/import-export/lib/methods/export.js builds the name and publishes it:
const date = dayjs().format('YYYYMMDDHHmmss');
const filename = `${self.apos.shortName}-${req.body.type.toLowerCase()}-export-${date}${format.extension}`;
const downloadPath = path.join('/exports', filename);
await copyIn(filepath, downloadPath);
The admin UI sends type as the module name (e.g. article), which matches the filename. The download URL is only shown in the exporter’s notification; nothing binds the HTTP request to that user afterward. After expiration (default 1000 * 60 * 10 ms) a timer deletes the object from uploadfs, which limits exposure but leaves a brute-forceable window.
Proof of concept
The script boots a minimal Apostrophe app with import-export, performs an export as the task user, then downloads the same archive without a session—first by path guessing, then with raw fetch. It exits non-zero on failure.
Requirements: Node 18+, pnpm install at the monorepo root (so apostrophe and @apostrophecms/import-export resolve). Set APOSTROPHE_ROOT to the checkout path.
#!/usr/bin/env bash
set -euo pipefail
# --- config ---
APOSTROPHE_ROOT="${APOSTROPHE_ROOT:-}"
if [[ -z "${APOSTROPHE_ROOT}" || ! -f "${APOSTROPHE_ROOT}/packages/import-export/package.json" ]]; then
echo "Set APOSTROPHE_ROOT to your apostrophe monorepo checkout." >&2
exit 1
fi
export APOS_TEST_DB_PROTOCOL=sqlite
cd "${APOSTROPHE_ROOT}/packages/import-export"
# --- run embedded PoC (no extra repo files) ---
node <<'NODE'
const assert = require('node:assert/strict');
const path = require('node:path');
const dayjs = require('dayjs');
const t = require('apostrophe/test-lib/util.js');
(async () => {
let apos;
const shortName = 'poc-export-site';
const exportsPath = (root) => path.join(root, 'public/uploads/exports');
try {
apos…
- Record
GHSA-5gc8-frw3-3vqm- CVSS 4.0
- 6.0 Medium
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-200
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
@apostrophecms/import-exportnpm |
before 3.7.1 | 3.7.1 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-112953 is a medium vulnerability (CVSS 6.0) in @apostrophecms Import-export, published October 20, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.56%, the 46th percentile. Fixed in @apostrophecms/import-export 3.7.1.
References
github.com/apostrophecms/apostrophe/security/advisories/GHSA-5gc8-frw3-3vqm
github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498
github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea
github.com/apostrophecms/apostrophe/releases/tag/@apostrophecms/[email protected]
nvd.nist.gov/vuln/detail/CVE-2026-112953
github.com/advisories/GHSA-5gc8-frw3-3vqm
github.com/CVEProject/cvelistV5/tree/main/cves/2026/112xxx/CVE-2026-112953.json
github.com/apostrophecms/apostrophe
Left out here: the rest of 2 source descriptions. The JSON record has them.