{"id":"CVE-2026-113662","kind":"vulnerability","aliases":["CVE-2026-113662","GHSA-jqw5-w6h3-44g6"],"related":[],"title":"ApostropheCMS import-export: NoSQL injection via update-key value lets a crafted import match and overwrite an unidentified document","description":"## Summary\n\n`@apostrophecms/import-export`'s update-by-key feature lets an imported\ndocument carry a `<field>: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.","summary":"The @apostrophecms/import-export package lets an archive's update-key field be used as a raw MongoDB query value, so a crafted operator object like { $ne: null } can match and overwrite an existing document the archive author did not identify. An attacker with a contributor-level import session can overwrite content in existing documents they have edit rights on.","severity":"medium","cvss":5.400000095367432,"epss":0.003000000026077032,"exploited":false,"exploited_at":null,"products":[{"name":"@apostrophecms/import-export","fixed":["3.7.0"],"affected":"< 3.7.0","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["apostrophecms","cms","cwe-284","cwe-943","node.js","package"],"not_affected_if":["unknown"],"how_to_check":"Check the installed version: npm ls @apostrophecms/import-export\nAffected if the version is below 3.7.0.","fix":"Upgrade @apostrophecms/import-export to 3.7.0 or later.","status":"open","published_at":"2026-10-20T18:17:19+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T19:58:16+00:00","updated_at":"2026-10-23T21:57:21+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T19:58:16Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade @apostrophecms/import-export to 3.7.0 or later.\nunknown","summary":"The @apostrophecms/import-export package lets an archive's update-key field be used as a raw MongoDB query value, so a crafted operator object like { $ne: null } can match and overwrite an existing document the archive author did not identify. An attacker with a contributor-level import session can overwrite content in existing documents they have edit rights on.","category":"package","who_cares":["apostrophecms","node.js","cms"],"confidence":0.8,"analyst_note":"","how_to_check":"Check the installed version: npm ls @apostrophecms/import-export\nAffected if the version is below 3.7.0.\nunknown","needs_analyst":false,"not_affected_if":["unknown"],"severity_if_unknown":"medium"},"cost_usd":0.000435,"input_tokens":2933,"output_tokens":284,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-04':429C '-09':428C '-113662':24A,32A '-2026':23A,31A '-284':37B '-943':39B '2026':427C '44g6':29A '79qf':204C,286C,452C '7xx3':206C,288C,454C 'access':267C 'actual':248C,374C 'admin':291C 'affect':418C 'alreadi':192C,314C,443C 'aposdocs.json':232C 'aposlocal':447C 'aposmod':160C 'apostroph':356C 'apostrophecm':1A,34B 'apostrophecms/import-export':33A,43B,102C 'appli':398C,444C 'archiv':47B,77B,166C,230C,244C 'articl':404C,414C 'attack':83B 'authent':263C 'author':78B,246C 'back':464C 'branch':423C 'bug':218C 'built':274C 'built-in':273C 'candid':354C 'carri':113C 'class':216C 'cms':35B 'confirm':420C 'const':154C 'contain':390C 'content':93B,397C 'contributor':87B,276C,384C 'contributor-level':86B 'contributor-rol':383C 'craft':14A,63B,222C 'cve':22A,30A 'cwe':36B,38B 'data':187C 'direct':144C 'doc':163C,342C,434C 'document':21A,75B,96B,112C,129C,167C,242C,311C,322C,373C,389C 'draft':159C,161C,403C 'edit':99B,327C 'ejson':171C 'entri':227C 'exact':303C 'exampl':118C 'exist':74B,95B,128C,241C,321C,401C 'export':4A 'fail':252C 'fall':463C 'featur':108C 'field':52B,318C,351C 'filter':360C 'find':126C,254C 'findforedit':358C 'findparkedaposdocid':200C 'findsingletonaposdocid':199C 'fix':193C,208C,432C,442C 'generic':326C 'get':375C 'getupdatekey':138C 'ghsa':26A,203C,285C,451C 'ghsa-79qf-vqgc-7xx3':202C,284C,450C 'ghsa-jqw5-w6h3-44g6':25A 'head':424C 'hold':278C 'ident':215C 'identifi':81B,249C,317C 'impact':257C 'import':3A,15A,89B,111C,235C,266C,313C,324C,387C,410C 'import-export':2A 'inject':6A 'insert':134C,467C 'insertorupdatedocwithkey':139C,212C 'instead':132C,250C,300C 'interact':292C 'jqw5':27A 'js':153C 'key':10A,51B,107C,115C,120C,224C,365C,392C 'know':315C 'launder':152C,433C 'let':12A,45B,109C 'level':88B 'lib/formats/gzip.js':173C 'lib/methods/import.js':140C 'like':66B 'limit':308C 'live':182C,380C 'live-verifi':379C 'logic':366C 'main':422C 'make':233C 'manager.findforediting':156C 'match':16A,70B,236C,256C,280C,295C,376C 'matchingdraftpromis':155C 'mechan':191C 'mirror':440C 'mode':158C 'modul':197C 'mongodb':58B,147C,298C 'narrow':371C 'ne':67B,179C,225C,336C,393C 'never':247C,411C 'new':136C 'node.js':40B 'non':346C 'non-nul':345C 'nosql':5A 'null':68B,180C,226C,337C,347C,394C 'object':65B,176C 'one':137C 'oper':64B,299C,334C 'overwrit':18A,72B,92B,405C 'overwritten':378C 'packag':41B,44B 'packages/import-export/lib/methods/import.js':430C 'parkedid':448C 'pars':169C 'path':468C 'permiss':359C 'plain':438C 'precondit':282C 'properti':116C 'queri':59B,148C,460C 'raw':57B 'reach':258C 'real':416C 'req.clone':157C 'request':386C 'requir':260C 'reviv':175C,190C 'right':100B,328C 'role':277C,385C 'satisfi':332C 'see':172C 'session':90B,264C 'slug':119C,223C,391C,417C 'sourc':188C 'specifi':412C 'string':439C,475C 'suggest':431C 'summari':101C 'surviv':219C 'target':270C 'thing':370C 'titl':407C 'toobject':165C 'touch':211C 'trivial':338C 'type':271C,446C 'unidentifi':20A 'unrel':402C 'untrust':186C 'updat':9A,50B,105C,131C,239C,364C 'update-by-key':104C 'update-key':8A,49B,363C 'updatefield':162C 'updatekey':164C,435C 'usabl':474C 'use':54B,124C,141C,296C,456C 'valu':11A,60B,122C,143C,149C,183C,304C,348C,461C 'verifi':381C 'version':419C 'via':7A 'vqgc':205C,287C,453C 'w6h3':28A 'whose':121C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-20T18:17:19+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-jqw5-w6h3-44g6","url":"https://github.com/advisories/GHSA-jqw5-w6h3-44g6","severity":"medium","published_at":"2026-10-20T18:19:19+00:00","modified_at":"2026-10-20T18:19:20+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-113662","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-113662","severity":null,"published_at":"2026-10-20T18:17:19+00:00","modified_at":"2026-10-20T19:53:10+00:00","first_seen_at":"2026-10-23T21:57:21+00:00","fetched_at":"2026-10-23T22:28:58+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}