GHSA-6h5j-32cf-4253 Apostrophe has Server-Side Prototype Pollution in apos.util.set via patch operators that leads to process-wide authorization bypass
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.DetailsRoot 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`:Source — `implementPatchOperators()` (`modules/@apostrophecms/schema/index.js` ~line 1737)User-controlled keys from the `$pullAll` operator are passed directly to `apos.util.set()`:`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)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: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 credentialsStep 1 — Confirm Endpoint Is Protected (Unauthenticated)Response:Step 2 — Obtain Editor TokenStep 3 — Poison `Object.prototype` via `$pullAll`Response:Step 4 — Authorization Bypass Confirmed (Unauthenticated)Response: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.CleanupThe pollution persists until the Node.js process is restarted.ImpactVulnerability Type**Server-Side Prototype Pollution leading to Authorization Bypass** (CWE-1321)Who Is ImpactedAny ApostropheCMS installation where at least one editor-level account exists.This is the default configuration for multi-user CMS deployments.Security ImpactA single PATCH request from an editor permanently modifies authorization behavior for the entire Node.js process.All subsequent unauthenticated requests to piece-type REST API endpoints bypass `publicApiCheck()`.Verified affected endpoints include:`@apostrophecms/user``@apostrophecms/global`Based on the shared authorization implementation, other piece-type REST endpoints appear similarly affected.The bypass affects every unauthenticated visitor until the server is restarted.Suggested FixReject dangerous prototype-related path segments before traversal:Apply the same validation both:Inside `apos.util.set()`Before passing user-controlled keys into `apos.util.set()` from `implementPatchOperators()`
CVSS v3.1 score: 9.1