CVE-2026-113659

mediumpriority · track · 0EPSS 0.003 · 28 %ileUS
grimoire priorityP3 · Track
Exploitable?
—
no known exploitation
Likely / severe?
0.34% (28th) EPSS
CVSS 6.1 · medium
Is it mine?
1 products
Check your exposure
Who says so?
1 sources
osv

EPSS · scored 2026-10-28

ApostropheCMS is an open-source Node.js content management system. Prior to 4.33.0, an unauthenticated attacker can send a crafted link whose query string is automatically parsed by apos.http.parseQuery when an authenticated victim loads a page with the admin bundle. The parser accepts the special __proto__, constructor, and prototype path keys, allowing browser prototype mutation that can reach an unsafe DOM sink and execute script in the victim's session; anonymous visitors do not load the affected admin context. This issue is fixed in version 4.33.0.

01

cvss

Technical severity, 0–10. One row per scoring source.

6.1
010

base score 0–10 · higher = more severe · hover a vector to decode

v3.16.1mediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:Nsecondary · security-advisories@github.comexploitability 2.8 · impact 2.7
v3.16.1mediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:Nsecondary · OSV
v3.15.6mediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N/E:Usecondary · CVSS-BT (t0sche)
02

ssvc

A stakeholder decision — act, attend, or track — from exploitation and impact.

Trackcoordinatorcisa_ssvcUS
03

weaknesses

The CWE weakness classes this flaw belongs to.

04

affected products (1)

Products and version ranges known to be affected.

05

advisories (1)

Vendor and ecosystem advisories describing this.

  • GHSA-m2m5-rw3w-cwmjApostropheCMS: Client-side Prototype Pollution in apos.http.parseQuery leading to DOM-based XSS against authenticated users2026-10-20ghsa · osvUS

    ## Summary The client-side helper `apos.http.parseQuery()` in ApostropheCMS builds a nested object from a URL query string without sanitizing the keys `__proto__`, `constructor`, or `prototype`. Parsing an attacker-controlled query string therefore writes arbitrary properties onto `Object.prototype` (client-side prototype pollution). ApostropheCMS's admin UI calls this parser with `window.location.search` on **every authenticated page load** (the admin context bar's `data()` initializer). Because the admin UI is built on Vue, a polluted, enumerable property on `Object.prototype` is then picked up by Vue's standard element-mount routine and written to the DOM as an element property, turning the prototype pollution into DOM-based XSS in the victim's authenticated session. The escalation gadget is a well-known, general Vue behavior; the novel, reportable defect is the unsanitized `parseQuery` source together with the authenticated trigger, both of which are ApostropheCMS code. Any user who can get **any logged-in user (from the lowest-privileged `guest` role up to the highest-privileged `admin` role)** to open a crafted URL can execute arbitrary JavaScript in that user's session. This is **distinct from CVE-2026-53609** (server-side prototype pollution via `apos.util.set()` / `$pullAll`, fixed in 4.31.0): different side (client vs server), different code path, and the vulnerable code here is present in the current release. ## Package - **Ecosystem:** npm - **Package:** `apostrophe` - **Affected versions:** all **4.x** releases, **4.0.0 through 4.32.2** (current `latest`). Verified by inspecting the published npm tarballs: `apostrophe@4.0.0` already contains the vulnerable descent (`parentKey = 'root'`) with no `__proto__`/`constructor`/`prototype` filtering, and `apostrophe@4.32.2` is unchanged in this respect. The vulnerable `parseQuery()` logic was introduced in commit `0255086bf` (2020-11-12), well before the 4.0 version, so **ApostropheCMS 3.x is also affected** (the same code ships in 3.x at the older build path `modules/@apostrophecms/util/ui/public/2-http.js`). - **Patched versions:** none at time of writing. ## Details The issue has three parts. Parts 1 and 2 are ApostropheCMS code and constitute the reportable vulnerability. Part 3 is the escalation gadget, which is standard Vue behavior and is included for completeness. ### 1. The pollution primitive: source (ApostropheCMS) `modules/@apostrophecms/util/ui/src/http.js`, function `apos.http.parseQuery()` (line ~301) and its inner `patch()` (lines ~317–354): ```js function patch(key, value) { let match; let parentKey = 'root'; let context = data; // data = {} (ordinary object) key = decodeURIComponent(key); const path = key.split('['); path.forEach(function(subKey) { if (subKey === ']') { /* array handling */ } else if (subKey.match(/^\d+]/)) { /* numeric index handling */ } else { match = subKey.replace(']', ''); if (!context[parentKey]) { // (A) truthy check, not an own-property check context[parentKey] = {}; } context = context[parentKey]; // (B) descends into whatever this resolves to parentKey = match; } }); value = (value === null) ? value : decodeURIComponent(value); if (Array.isArray(context[parentKey])) { context[parentKey].push(value); } else if (context[parentKey] !== undefined) { context[parentKey] = [ context[parentKey], value ]; } else { context[parentKey] = value; // (C) assignment onto the resolved object } } ``` No `__proto__`, `constructor`, or `prototype` key is rejected. Walking the input `__proto__[x]=v`: 1. Segment `__proto__`: at **(A)**, `context['__proto__']` resolves to the object's inherited prototype (truthy), so the `{}` initializer is skipped; at **(B)**, `context` becomes `Object.prototype`. 2. Segment `x`: at **(C)**, `Object.prototype['x'] = v`. The truthy guard at **(A)** (rather than an own-property check) is what allows the descent into the inherited prototype. Because the assignment at **(C)** is an ordinary property write, the resulting `Object.prototype` property is **enumerable**,which matters for the gadget (3). ### 2. The trigger: reached on every authenticated page load (ApostropheCMS) `modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue`. The component's `data()` initializer (line ~64) runs when the admin context bar mounts (i.e. on every authenticated page load), and passes the raw query string straight into the vulnerable parser: ```js data() { const query = apos.http.parseQuery(location.search); // <- attacker-controlled -> (1) pollution, on mount const draftMode = query.aposMode || 'published'; // ... } ``` The same component's `refresh()` method (line ~606) passes `window.location.search` into `parseQuery()` again on admin-bar refresh events, and folds it into `doc._url` (re-parsed inside `apos.http.remote()`), providing additional trigger points. The admin context bar mounts only when a session is present and the admin bundle loads, so none of this executes for anonymous visitors, which is why the pollution is observed **only when logged in**. All of this is core ApostropheCMS code (`node_modules/apostrophe/...`); it is not introduced by any project or starter kit content. ### 3. The escalation gadget: Vue element mount (framework behavior; known, not novel) The demonstrated payload pollutes the property `innerHTML` (`__proto__[innerHTML]=<img/src/onerror=alert(1)>`). The sink that consumes it is **Vue's standard element-mount path**, not ApostropheCMS code. Confirmed in the bundled framework (`vue` / `@vue/runtime-core` / `@vue/runtime-dom` all **3.5.42**): `@vue/runtime-core` `mountElement`, prop enumeration (line ~5741): ```js if (props) { for (const key in props) { // for...in walks the prototype chain if (key !== "value" && !shared.isReservedProp(key)) { // isReservedProp does NOT list innerHTML hostPatchProp(el, key, null, props[key], ...); // props["innerHTML"] → inherited polluted value } } ``` `@vue/runtime-dom` `patchDOMProp` (line ~583), reached via `hostPatchProp`: ```js function patchDOMProp(el, key, value, ...) { if (key === "innerHTML" || key === "textContent") { if (value != null) { el[key] = key === "innerHTML" ? unsafeToTrustedHTML(value) : value; // el.innerHTML = payload } return; } ``` Mechanism: whenever Vue mounts an element that has a non-null `props` object without its own `innerHTML` (routine during the admin render), `for (const key in props)` enumerates the inherited, enumerable `innerHTML` from `Object.prototype`; it passes the `"value"` and `isReservedProp` filters; `props["innerHTML"]` resolves to the attacker's payload; and `patchDOMProp` writes it to `el.innerHTML`. `unsafeToTrustedHTML` is Vue's Trusted-Types wrapper, not a sanitizer, a passthrough unless Trusted Types is enforced (not the default). This gadget is a well-documented, essentially universal client-side-PP > XSS escalation in Vue applications. It is **not claimed as a novel finding** and is **not ApostropheCMS's to fix**, it cannot be removed without changing Vue. Its relevance here is that it makes the (1) pollution reliably exploitable in any ApostropheCMS admin session. ## Proof of Concept Prerequisite: a running ApostropheCMS instance and any authenticated session (a `guest` account suffices). The tester/victim must be logged in. **A. Prototype pollution (console-verifiable):** 1. Log in as any user. 2. Navigate to: `https://TARGET/?__proto__[prop]=polluted` 3. In the browser console: `Object.prototype.prop` > returns `"polluted"`. **B. DOM-based XSS:** 1. Log in as any user. 2. Navigate to: `https://TARGET/?__proto__[innerHTML]=%3Cimg/src/onerror%3dalert(1)%3E` 3. `alert(1)` executes in the victim's authenticated origin. To capture the sink stack for evidence: DevTools > Sources > open the admin bundle > pretty-print > search `unsafeToTrustedHTML` (or `=== "innerHTML"`) > line breakpoint in the `patchDOMProp` innerHTML branch > reload the logged-in payload URL > inspect `value` and walk the call stack up through `hostPatchProp` to `mountElement`'s `for…in`. Both A and B were reproduced against ApostropheCMS 4.32.2 (Vue 3.5.42) in Chrome, Firefox, and Edge browsers. ## Impact Any party who can coerce an authenticated ApostropheCMS user to open a crafted URL can execute arbitrary JavaScript in that user's session. Because the escalation gadget is a guaranteed-present Vue behavior, this is a reliable exploit, not a theoretical primitive. Consequences scale with the victim's role: - **Any authenticated victim:** actions and data reads available to that account; theft of the session cookie / CSRF token from the authenticated context. - **Editor/admin victim:** the injected script can invoke the authenticated management REST APIs as the victim, creating or modifying documents and user accounts, changing configuration and possibly leading to account takeover and potential full compromise of the CMS instance. Prototype pollution can additionally affect unrelated code paths that read inherited properties, so other gadgets beyond the demonstrated Vue `innerHTML` sink may exist. ## Remediation The fix belongs in ApostropheCMS, at the source (1). The gadget (3) is framework behavior and cannot be relied upon to change, so sanitizing `parseQuery` is the correct fix. **Primary fix: reject prototype-affecting keys in `parseQuery`/`patch()`**: ```js apos.http.parseQuery = function(query) { query = query.replace(/^\?/, ''); const data = {}; const DANGEROUS = new Set([ '__proto__', 'constructor', 'prototype' ]); query.split('&').forEach(function(pair) { // ... unchanged ... }); return data.root || {}; function patch(key, value) { let match; let parentKey = 'root'; let context = data; key = decodeURIComponent(key); const path = key.split('['); let dangerous = false; path.forEach(function(subKey) { const cleanKey = subKey.replace(']', ''); if (DANGEROUS.has(cleanKey)) { // refuse to traverse into prototype-affecting keys dangerous = true; return; } // ... existing subKey handling ... }); if (dangerous) { return; // discard this parameter entirely } // ... existing assignment ... } }; ``` **Defense in depth:** - Replace the truthy guard at **(A)** with an own-property check: `if (!Object.prototype.hasOwnProperty.call(context, parentKey))`, so a key can never descend into an inherited prototype. - Build `data` and any created containers with `Object.create(null)` so intermediate objects have no prototype chain to reach. **Interim mitigation for operators:** restrict who holds authenticated accounts, and be aware that opening untrusted links while logged in can trigger this. There is no server-side configuration toggle that disables the vulnerable client behavior. ## References - Source (ApostropheCMS): `apostrophe/modules/@apostrophecms/util/ui/src/http.js` — `parseQuery()` / `patch()` - Trigger (ApostropheCMS): `apostrophe/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue` — `data()` (~line 64), `refresh()` (~line 606) - Escalation gadget (Vue 3.5.42, framework behavior): `@vue/runtime-core` `mountElement` prop enumeration (~line 5741); `@vue/runtime-dom` `patchDOMProp` innerHTML branch (~line 583) - Related but distinct: CVE-2026-53609 / GHSA-6h5j-32cf-4253 (server-side prototype pollution via `apos.util.set()` / `$pullAll`, fixed in 4.31.0) - CWE-1321; CWE-79 ## Credit ### Github Account Username: `Xx-LoGiCaL-xX` ---

06

sources

Which sources assert this record, and when.

published 2026-10-20modified 2026-10-28
07

references (4) nvd

External citations — patches, advisories, analysis.

scry

Attackers weaponise vulnerabilities like this against internet-exposed services. See what is exposed on your perimeter with a free passive scan.

Free scan