npm · @apostrophecms/import-export
ApostropheCMS import-export: NoSQL injection via update-key value lets a crafted import match and overwrite an unidentified document
@apostrophecms/import-export's update-by-key feature lets an imported
document carry a <field>:key property (for example slug:key) whose
value is used to find an existing document to update instead of inserting
a new one. getUpdateKey()/insertOrUpdateDocWithKey()
(lib/methods/import.js) use that value directly as a MongoDB query value
with no laundering:
const matchingDraftPromise = manager.findForEditing(
req.clone({ mode: 'draft' }),
{
aposMode: 'draft',
[updateField]: doc[updateKey]
}).toObject();
Archive documents are parsed with EJSON (see lib/formats/gzip.js), which
revives objects such as { $ne: null } as live values, the same untrusted
data source and revival mechanism already fixed once in this module for
findSingletonAposDocId/findParkedAposDocId in GHSA-79qf-vqgc-7xx3. That
fix did not touch insertOrUpdateDocWithKey, so the identical class of bug
survives here: a crafted "slug:key": { "$ne": null } entry in an
archive's aposDocs.json makes the import match (and then update) an
existing document the archive's author never actually identified, instead
of failing to find a match.
Reaching this requires only an authenticated session with import access to
the target type (the built-in contributor role holds this, matching the
precondition of GHSA-79qf-vqgc-7xx3), and no admin interaction. Because the
match uses a MongoDB operator instead of an exact value, it is not limited
to a document the importer already knows the identifying field of: any
existing document the importer has generic edit rights on and that
satisfies the operator (which { $ne: null } trivially does for any doc
with a non-null value in that field) is a candidate, and Apostrophe's
findForEditing permission filter, not the update-key logic, is the only
thing narrowing which document actually gets matched and overwritten.
Live-verified: a contributor-role request importing a document containing
"slug:key": { "$ne": null } had its content applied to an existing,
unrelated draft article, overwriting its title, while the importer never
specified that article's real slug.
Confirmed on main branch HEAD as of 2026-09-04,
packages/import-export/lib/methods/import.js.
Launder doc[updateKey] to a plain string (mirroring the fix already
applied to type/aposLocale/parkedId in GHSA-79qf-vqgc-7xx3) before
using it as a query value, and fall back to the insert path when it is not
a usable string.
Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.
Check my repoSources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.