{"id":"CVE-2026-113648","kind":"vulnerability","aliases":["CVE-2026-113648","GHSA-rw26-wwfc-9wvm"],"related":[],"title":"ApostropheCMS: Missing Authentication in `cleanExport` Route","description":"#### 1. Summary\nThe API endpoint `POST /api/v1/@apostrophecms/import-export/clean-export` in `@apostrophecms/import-export` lacks authentication and authorization checks. Any client—including unauthenticated anonymous users who have obtained the standard CSRF cookie from visiting the site—can directly invoke this route and trigger the underlying `self.cleanExport(req)` method.\n\n---\n\n#### 2. Affected Files & Endpoints\n* **Endpoint:** `POST /api/v1/@apostrophecms/import-export/clean-export`\n* **Route Definition:** `packages/import-export/lib/apiRoutes.js` (lines 32–34)\n* **Method Implementation:** `packages/import-export/lib/methods/import.js` (lines 1271–1324)\n\n---\n\n#### 3. Root Cause Analysis\nIn `packages/import-export/lib/apiRoutes.js`, the `get.related` handler explicitly validates that the caller is logged in:\n```javascript\nget: {\n  async related(req) {\n    if (!req.user) {\n      throw self.apos.error('forbidden');\n    }\n    // ...\n  }\n}\n```\nIn contrast, `post.cleanExport` (as well as `post.overrideDuplicates`) lacks any verification of `req.user`:\n```javascript\npost: {\n  async overrideDuplicates(req) {\n    return self.overrideDuplicates(req);\n  },\n\n  async cleanExport(req) {\n    return self.cleanExport(req); // <-- No authentication check\n  }\n}\n```\nBecause ApostropheCMS does not enforce authentication globally on all `apiRoutes`, omitting an explicit `if (!req.user)` guard allows anonymous requests to pass directly through to the business logic layer.\n\n---\n\n#### 4. Evidence\nWhen sending an unauthenticated POST request with the site’s CSRF cookie (issued to any visitor on `GET /`), the request bypasses access control entirely and executes `self.cleanExport(req)`, as demonstrated by the server stack trace:\n\n```text\nat Object.cleanExport (.../packages/import-export/lib/methods/import.js)\nat .../packages/apostrophe/modules/@apostrophecms/module/index.js:193:30\n```\nThis confirms that execution directly reaches the core import-export handler without an authenticated session.\n\n---\n\n#### 5. Impact\n1. **Broken Access Control:** Unauthenticated users can invoke cleanup operations and delete temporary export files and artifacts stored on the server.\n2. **Unauthorized State Mutation:** The endpoint allows modifying job records in the database via `req.body.jobId` (forcing them to `ended: true, status: 'completed'`) and dismissing notifications via `req.body.notificationId` without verifying whether the resources belong to the requesting caller.\n\n---\n\n#### 6. Suggested Remediation (Patch)\n\nEnforce an authentication check in `packages/import-export/lib/apiRoutes.js` before executing the underlying methods:\n\n```diff\n--- a/packages/import-export/lib/apiRoutes.js\n+++ b/packages/import-export/lib/apiRoutes.js\n@@ -26,10 +26,16 @@ module.exports = self => {\n     get: { ... },\n     post: {\n       async overrideDuplicates(req) {\n+        if (!req.user) {\n+          throw self.apos.error('forbidden');\n+        }\n         return self.overrideDuplicates(req);\n       },\n \n       async cleanExport(req) {\n+        if (!req.user) {\n+          throw self.apos.error('forbidden');\n+        }\n         return self.cleanExport(req);\n       }\n     }\n```","summary":"The ApostropheCMS import-export module exposes POST /api/v1/@apostrophecms/import-export/clean-export without an authentication check, so anonymous visitors holding the site's CSRF cookie can trigger cleanup. An attacker can delete temporary export files on the server and mark arbitrary import/export job records as completed or dismiss notifications.","severity":"low","cvss":3.1,"epss":0.003280000062659383,"exploited":false,"exploited_at":null,"products":[{"name":"@apostrophecms/import-export","fixed":["3.7.1"],"affected":"< 3.7.1","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["apostrophecms","cwe-306","cwe-862","node","package","self-hosted"],"not_affected_if":["import-export-not-installed","managed-hosting","version-3.7.1-or-later"],"how_to_check":"Check whether @apostrophecms/import-export is below 3.7.1 in package-lock.json or the installed node_modules.\nSend an unauthenticated POST to /api/v1/@apostrophecms/import-export/clean-export with only the CSRF cookie and see whether it returns success instead of forbidden.","fix":"Upgrade @apostrophecms/import-export to 3.7.1 or later.\nUntil then, add an if (!req.user) guard to the cleanExport and overrideDuplicates handlers in lib/apiRoutes.js as in the advisory patch.","status":"open","published_at":"2026-10-20T17:17:35+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T20:03:35+00:00","updated_at":"2026-10-23T21:57:20+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T20:03:35Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade @apostrophecms/import-export to 3.7.1 or later.\nUntil then, add an if (!req.user) guard to the cleanExport and overrideDuplicates handlers in lib/apiRoutes.js as in the advisory patch.","summary":"The ApostropheCMS import-export module exposes POST /api/v1/@apostrophecms/import-export/clean-export without an authentication check, so anonymous visitors holding the site's CSRF cookie can trigger cleanup. An attacker can delete temporary export files on the server and mark arbitrary import/export job records as completed or dismiss notifications.","category":"package","who_cares":["apostrophecms","node","self-hosted"],"confidence":0.8,"analyst_note":"","how_to_check":"Check whether @apostrophecms/import-export is below 3.7.1 in package-lock.json or the installed node_modules.\nSend an unauthenticated POST to /api/v1/@apostrophecms/import-export/clean-export with only the CSRF cookie and see whether it returns success instead of forbidden.","needs_analyst":false,"not_affected_if":["managed-hosting","import-export-not-installed","version-3.7.1-or-later"],"severity_if_unknown":"low"},"cost_usd":0.000536,"input_tokens":3207,"output_tokens":430,"severity_before":"low"},"analyst_status":null,"analyst_note":null,"search":"'+26':366C '-113648':9A,17A '-2026':8A,16A '-26':364C '-306':21B '-862':23B '/api/v1':37B,82C,126C '/packages/apostrophe/modules':266C '/packages/import-export/lib/methods/import.js':264C '1':76C,288C '10':365C '1271':138C '1324':139C '16':367C '193':268C '2':120C,309C '3':140C '30':269C '32':132C '34':133C '4':224C '5':286C '6':346C '9wvm':14A 'a/packages/import-export/lib/apiroutes.js':362C 'access':247C,290C 'affect':121C 'allow':212C,315C 'analysi':143C 'anonym':44B,95C,213C 'api':79C 'apirout':205C 'apostrophecm':1A,19B,30B,197C 'apostrophecms/import-export':18A,85C 'apostrophecms/import-export/clean-export':38B,83C,127C 'apostrophecms/module/index.js':267C 'arbitrari':67B 'artifact':304C 'async':159C,181C,187C,372C,383C 'attack':56B 'authent':3A,41B,87C,194C,201C,284C,352C 'author':89C 'b/packages/import-export/lib/apiroutes.js':363C 'belong':341C 'broken':289C 'busi':221C 'bypass':246C 'caller':153C,345C 'caus':142C 'check':42B,90C,195C,353C 'cleanexport':5A,188C,384C 'cleanup':54B,296C 'client':92C 'complet':72B,330C 'confirm':271C 'contrast':168C 'control':248C,291C 'cooki':51B,103C,237C 'core':277C 'csrf':50B,102C,236C 'cve':7A,15A 'cwe':20B,22B 'databas':321C 'definit':129C 'delet':58B,299C 'demonstr':255C 'diff':361C 'direct':109C,217C,274C 'dismiss':74B,332C 'end':327C 'endpoint':80C,123C,124C,314C 'enforc':200C,350C 'entir':249C 'evid':225C 'execut':251C,273C,357C 'explicit':149C,208C 'export':33B,60B,280C,301C 'expos':35B 'file':61B,122C,302C 'forbidden':166C,379C,390C 'forc':324C 'get':158C,243C,370C 'get.related':147C 'ghsa':11A 'ghsa-rw26-wwfc-9wvm':10A 'global':202C 'guard':211C 'handler':148C,281C 'hold':46B 'host':28B 'impact':287C 'implement':135C 'import':32B,279C 'import-export':31B,278C 'import/export':68B 'includ':93C 'invok':110C,295C 'issu':238C 'javascript':157C,179C 'job':69B,317C 'lack':86C,174C 'layer':223C 'line':131C,137C 'log':155C 'logic':222C 'mark':66B 'method':119C,134C,360C 'miss':2A 'modifi':316C 'modul':34B 'module.exports':368C 'mutat':312C 'node':24B 'notif':75B,333C 'object.cleanexport':263C 'obtain':99C 'omit':206C 'oper':297C 'overridedupl':182C,373C 'packag':25B 'packages/import-export/lib/apiroutes.js':130C,145C,355C 'packages/import-export/lib/methods/import.js':136C 'pass':216C 'patch':349C 'post':36B,81C,125C,180C,230C,371C 'post.cleanexport':169C 'post.overrideduplicates':173C 'reach':275C 'record':70B,318C 'relat':160C 'remedi':348C 'req':118C,161C,183C,186C,189C,192C,253C,374C,382C,385C,393C 'req.body.jobid':323C 'req.body.notificationid':335C 'req.user':163C,178C,210C,376C,387C 'request':214C,231C,245C,344C 'resourc':340C 'return':184C,190C,380C,391C 'root':141C 'rout':6A,112C,128C 'rw26':12A 'self':27B,369C 'self-host':26B 'self.apos.error':165C,378C,389C 'self.cleanexport':117C,191C,252C,392C 'self.overrideduplicates':185C,381C 'send':227C 'server':64B,258C,308C 'session':285C 'site':48B,107C,234C 'stack':259C 'standard':101C 'state':311C 'status':329C 'store':305C 'suggest':347C 'summari':77C 'temporari':59B,300C 'text':261C 'throw':164C,377C,388C 'trace':260C 'trigger':53B,114C 'true':328C 'unauthent':94C,229C,292C 'unauthor':310C 'under':116C,359C 'user':96C,293C 'valid':150C 'verif':176C 'verifi':337C 'via':322C,334C 'visit':105C 'visitor':45B,241C 'well':171C 'whether':338C 'without':39B,282C,336C 'wwfc':13A","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-20T17:17:35+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-rw26-wwfc-9wvm","url":"https://github.com/advisories/GHSA-rw26-wwfc-9wvm","severity":"low","published_at":"2026-10-21T17:13:28+00:00","modified_at":"2026-10-21T17:13:28+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-113648","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-113648","severity":null,"published_at":"2026-10-20T17:17:35+00:00","modified_at":"2026-10-23T02:16:55+00:00","first_seen_at":"2026-10-23T21:57:20+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"}