{"id":"GHSA-jqw5-w6h3-44g6","summary":"ApostropheCMS import-export: NoSQL injection via update-key value lets a crafted import match and overwrite an unidentified document","details":"## Summary\n\n`@apostrophecms/import-export`'s update-by-key feature lets an imported\ndocument carry a `\u003cfield\u003e:key` property (for example `slug:key`) whose\nvalue is used to find an existing document to update instead of inserting\na new one. `getUpdateKey()`/`insertOrUpdateDocWithKey()`\n(`lib/methods/import.js`) use that value directly as a MongoDB query value\nwith no laundering:\n\n```js\nconst matchingDraftPromise = manager.findForEditing(\n  req.clone({ mode: 'draft' }),\n  {\n    aposMode: 'draft',\n    [updateField]: doc[updateKey]\n  }).toObject();\n```\n\nArchive documents are parsed with EJSON (see `lib/formats/gzip.js`), which\nrevives objects such as `{ $ne: null }` as live values, the same untrusted\ndata source and revival mechanism already fixed once in this module for\n`findSingletonAposDocId`/`findParkedAposDocId` in GHSA-79qf-vqgc-7xx3. That\nfix did not touch `insertOrUpdateDocWithKey`, so the identical class of bug\nsurvives here: a crafted `\"slug:key\": { \"$ne\": null }` entry in an\narchive's `aposDocs.json` makes the import match (and then update) an\nexisting document the archive's author never actually identified, instead\nof failing to find a match.\n\n## Impact\n\nReaching this requires only an authenticated session with import access to\nthe target type (the built-in contributor role holds this, matching the\nprecondition of GHSA-79qf-vqgc-7xx3), and no admin interaction. Because the\nmatch uses a MongoDB operator instead of an exact value, it is not limited\nto a document the importer already knows the identifying field of: any\nexisting document the importer has generic edit rights on and that\nsatisfies the operator (which `{ $ne: null }` trivially does for any doc\nwith a non-null value in that field) is a candidate, and Apostrophe's\n`findForEditing` permission filter, not the update-key logic, is the only\nthing narrowing which document actually gets matched and overwritten.\n\nLive-verified: a contributor-role request importing a document containing\n`\"slug:key\": { \"$ne\": null }` had its content applied to an existing,\nunrelated draft article, overwriting its title, while the importer never\nspecified that article's real slug.\n\n## Affected versions\n\nConfirmed on `main` branch HEAD as of 2026-09-04,\n`packages/import-export/lib/methods/import.js`.\n\n## Suggested fix\n\nLaunder `doc[updateKey]` to a plain string (mirroring the fix already\napplied to `type`/`aposLocale`/`parkedId` in GHSA-79qf-vqgc-7xx3) before\nusing it as a query value, and fall back to the insert path when it is not\na usable string.","aliases":["CVE-2026-113662"],"modified":"2026-10-20T18:30:03.585162852Z","published":"2026-10-20T18:19:19Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-284","CWE-943"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-10-20T18:19:19Z"},"references":[{"type":"WEB","url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-jqw5-w6h3-44g6"},{"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/@apostrophecms/import-export@3.7.0"}],"affected":[{"package":{"name":"@apostrophecms/import-export","ecosystem":"npm","purl":"pkg:npm/%40apostrophecms/import-export"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"3.7.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-jqw5-w6h3-44g6/GHSA-jqw5-w6h3-44g6.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L"}]}