Open actionable advisories and recorded vendor fixes. This public package view has no inventory attached and cannot establish whether your deployment is affected. A vendor fix never resolves your finding; verify your installed version and the source guidance.
Open advisories
32 actionable advisories, newest first. Withdrawn advisories and news are excluded. Coverage is partial.
The slugTaken endpoint returns a boolean indicating whether a slug is in use across all document types and visibility levels, with no permission or type filter. Any logged-in user (including editors) can enumerate the slug space of private pages, drafts, and archived content. Details POST /api/v1/@apostrophecms/doc/slugTaken with body { "slug": "target-slug" } runs a raw existence check against the documents collection. It does not: - Check whether the requesting user has read permission for documents of that type. - Filter by visibility (public, loginRequired, private). - Filter by viewRole. - Filter by publication status (draft vs published). A single authenticated session can iterate the slug space to enumerate the full content inventory of the CMS, including content the user is explicitly prohibited from viewing. PoC Impact Information disclosure. An attacker with any authenticated session can enumerate all slugs in the system, mapping the full content structure of the…
Recommended action: Upgrade to 4.34.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
The getOne REST endpoint for pages does not apply viewRole type-level restrictions or viewPermission field-level redaction. An editor-level user who knows a document id can read the full content of admin-only pages and their privileged fields, while the getAll endpoint correctly enforces both restrictions. Details Finding 3 — viewRole bypass: modules/@apostrophecms/page/index.js exposes getRestQuery(req, omitPermissionCheck = false). The getAll path calls publicApiCheckAsync and threads the full query builder pipeline including viewRole filtering. The getOne path uses a divergent code path that skips this pipeline. A page type declared with viewRole: 'admin' is inaccessible via GET /api/v1/@apostrophecms/page?type=secretPage (returns 403 to an editor) but is fully returned by GET /api/v1/@apostrophecms/page/:id:en:published to the same editor. Finding 4 — viewPermission field leak: Fields decorated with viewPermission: { action: 'edit', type: '@apostrophecms/page' } are…
Recommended action: Upgrade to 4.34.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
ApostropheCMS widget styles support project-defined schema fields. If a project defines a free-form string style field with a CSS property and no selector, an editor-controlled value is rendered as an inline CSS declaration. The resulting declaration is then inserted into the widget wrapper's style="..." attribute without HTML-attribute escaping. An attacker who can edit the widget can include a double quote in the style value, terminate the style attribute, and inject new HTML event-handler attributes. The payload is stored with the widget and executes in the Apostrophe site origin whenever the affected published page is rendered. This is a sibling of CVE-2026-33889 / GHSA-97v6-998m-fp4g. The earlier issue escaped from a <style element through a malicious color value. This variant reaches a different sink related to the automatically generated widget wrapper's inline style and class attributes. Details Root Cause 1: Free-form string style values retain HTML metacharacters…
Recommended action: Upgrade to 4.34.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
ApostropheCMS lets a module expose an anonymous read API by setting publicApiProjection, which whitelists the document fields unauthenticated callers may see. The public REST GET handler applies every launderable query builder from the query string before forcing that projection. Two of those builders, search and sort, operate on the underlying documents rather than the projected output, and neither is checked against the projection. An unauthenticated attacker can therefore: - use ?search=<word as a value oracle to confirm and locate documents by the content of a field the projection was meant to hide, and - use ?sort[hiddenField]=1 as an ordering oracle to rank documents by a hidden field's value. This is the same confidentiality-bypass class already patched for the choices/counts builders (gated by choicesFieldAllowedByProjection). That gate was never applied to search or sort. Details For an anonymous caller, getRestQuery (piece types:…
Recommended action: Upgrade to 4.34.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
The color field accepts any string that begins with a valid rgb() / hsl() color, because TinyColor.isValid does not anchor its match at the end of the string. An authenticated user with edit rights can store arbitrary HTML after a valid color prefix. When the stored value is rendered , the payload executes, resulting in stored XSS. Details Server-side validation of the color field relies on TinyColor: TinyColor's rgb/hsl matchers are not anchored at the end of the string, while hex matchers are. Tested with @ctrl/tinycolor | Input | isValid | |---|---| | rgb(1,2,3)<img src=x onerror=alert(1) | true | | rgb(1,2,3)</textarea<scriptalert(1)</script | true | | hsl(0,100%,50%);--x:1}</style<scriptalert(1)</script | true | | #ff0000<img src=x onerror=alert(1) | false | The value is saved to MongoDB unchanged (not normalized to hex8). Relation to GHSA-97v6-998m-fp4g GHSA-97v6 addressed CSS custom property injection escaping a <style context. The root cause (validation that accepts…
Recommended action: Upgrade to 4.34.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
Review scope and evidence limits Package: @apostrophecms/import-export, version 3.7.0. Release tag reviewed: @apostrophecms/import-export@3.7.0, commit 9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce (September 30, 2026). Current main reviewed: 5ce3732be0ebdebb4db0f8ef160c81e8671157e6. The importer, gzip extractor, import permission gate, and core permission implementation have no differences between that release tag and the reviewed main. This follow-up is a static remediation review. We have not repeated the original end-to-end reproductions against the latest release or tested a deployed site. Current exploitability and impact therefore still require maintainer validation; the observations below establish that the reported security checks remain absent in public source. The original emailed PDF contains the original reproduction material. Import API bypasses publication roles for protected content The import gate in canImportOrExport() checks the type's import configuration and view…
Recommended action: Upgrade to 4.34.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
A potential unauthenticated denial-of-service vulnerability exists in the chunked import upload path used by @apostrophecms/import-export. The affected import API routes invoke: self.apos.http.bigUploadMiddleware() before the actual import handler performs its authentication check. Because no authorize callback is supplied to bigUploadMiddleware(), unauthenticated requests can reach the aposBigUpload start, chunk, and end operations before the later req.user check rejects the import. As a result, an unauthenticated remote attacker can create server-side upload state, write chunk data to upload storage, trigger temporary-file processing, and cause attacker-controlled cleanup operations before authentication is enforced. Affected Component Package: @apostrophecms/import-export Confirmed affected version: 3.6.2 Potential earlier affected versions: The issue may affect earlier 3.x releases in which the current big-upload-based import implementation is present. I have not yet…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
Impact Even though it is correctly applied to guests, contributors, etc., viewRole is not applied to the general public. This means that a secured content type like @apostrophecms/user can in theory be accessed. This is greatly mitigated by the fact that there is no automatic public REST API for any content type. One has to expressly allow it. However, it does mean that developers can make the mistake of setting up a relationship between a content type like a blog post, and @apostrophecms/user. This could potentially expose some sensitive information if templates were written poorly, e.g. if they JSON-serialized the entire object. Note that password hashes are NOT affected because they reside in a separate collection. The public demo project and its various flavors must be updated first because it inadvertently relies on the bug. Patches 4.33.0 Workarounds Never create relationships with @apostrophecms/user that are intended for non-admin display. If you need a relationship that…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
The notification module exposes PATCH and DELETE REST endpoints that perform operations directly on the underlying MongoDB collection without verifying that the requester is authenticated or owns the target notification. As a result, an unauthenticated attacker who obtains a valid notification ID can modify or permanently delete another user's notification. --- Affected Code File Unlike the GET notification route, these handlers do not verify: - Authentication (req.user) - Ownership (userId === req.user.id) - Authorization or permissions Instead, the supplied id is used directly against the raw MongoDB collection. --- Root Cause The notification REST endpoints perform database operations directly using self.db without enforcing authentication or authorization. While the framework provides permission primitives (req.user, ownership checks, and apos.permission), these endpoints never invoke them. As a result, any request reaching these handlers is processed solely based on the…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
ApostropheCMS’s exist-in-locale API allows an authenticated low-privilege user to determine whether restricted documents exist in a specified locale and mode, including draft mode. The endpoint returns localized document identifiers without applying document-level view or edit authorization criteria. The issue was reproduced against a real local ApostropheCMS application running the reviewed source code. The test used a synthetic guest account and synthetic restricted draft records. Details Exact vulnerable source path: packages/apostrophe/modules/@apostrophecms/i18n/index.js:503-529 Exact API route: POST /api/v1/@apostrophecms/i18n/exist-in-locale The handler verifies only that the requester is authenticated: It then performs a direct database lookup using attacker-controlled document IDs, locale, and mode: This query does not apply: .permission('view') .permission('edit') apos.permission.criteria(req, 'view') object-level document authorization locale-specific authorization…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
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…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
The self-service password-change endpoint verifies the current password but does not use the login module’s failed-attempt counter, throttle, delay, or lockout mechanism. An attacker with a valid authenticated session can submit repeated guesses for the current password without receiving a lockout or throttle response. Exact source path packages/apostrophe/modules/@apostrophecms/settings/index.js:621-664 The password-change route is: PATCH /api/v1/@apostrophecms/settings/password The route calls the password verifier directly through: await self.handleProtectedSubform(req, subform, req.body ); The password verification path is: packages/apostrophe/modules/@apostrophecms/settings/index.js:514-542 async handlePasswordProtectedSubform(req, subform, payload) { try { await self.apos.user.verifyPassword(req.user, payload.passwordCurrent); } catch (e) { throw self.apos.error( 'forbidden', 'apostrophe:passwordCurrentError', { path: 'passwordCurrent' } ); } return subform; } The…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
The REST route GET /api/v1/<piece-type/:id/locales (and the equivalent page route) does not apply the publicApiCheckAsync gate that ApostropheCMS's other read routes enforce. An unauthenticated caller can therefore query this route for any document id and learn whether the document exists and which locales it is published in — a document-existence and locale-metadata disclosure that the read-API is otherwise supposed to gate. Affected - Repository: apostrophecms/apostrophe (monorepo package apostrophe). - Confirmed at 4.32.2 (commit d3ff8c9 == the shipped 4.32.2; unchanged at current HEAD). No fix at time of report. - Stack: Node.js / Express + MongoDB. - Endpoints: GET /api/v1/<piece-type/:id/locales and GET /api/v1/@apostrophecms/page/:id/locales. Technical detail - Unsafe: modules/@apostrophecms/piece-type/index.js:397 registers the :id/locales route with no publicApiCheckAsync. The page module has the same omission at modules/@apostrophecms/page/index.js:925. - Gated…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
The core page REST API reads full page documents with .permission(false) in getOne and in the all=1 tree dump, bypassing page-type viewRole, visibility: private, and visibility: loginRequired checks. Unauthenticated visitors and low-privilege contributors can read admin-only and non-public page content by id/slug. Details packages/apostrophe/modules/@apostrophecms/page/index.js: - getOne (L456–465): await self.publicApiCheckAsync(req); ... self.getRestQuery(req).permission(false).and(criteria) — no viewRole threshold, no visibility filtering. - all handler (L318–395): getRestQuery(req).permission(false) (L384–385) walks every page type and returns the projection including visibility. - publicApiCheckAsync (L3044) only checks that publicApiProjection exists — it performs no authorization. - Correct control behaves differently: piece-type/index.js:getOne (L319–327) uses getRestQuery(req) without .permission(false) and enforces viewRole. PoC Impact Unauthenticated disclosure of…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
A rejected upload can remain in the operating system temporary directory. Multer writes the file before its extension is checked, and the rejection path does not remove that temporary file. An unauthenticated requester can repeat this with large files and steadily consume temporary disk space. Details The endpoint is: In packages/apostrophe/modules/@apostrophecms/rich-text-widget/lib/apiRoutes.js, Multer first writes the upload to os.tmpdir(). The handler then rejects filenames whose extension is not csv, but there is no cleanup of the Multer file on this error path. The route does not require an authenticated user. A normal CSRF cookie is sufficient. During testing, the request returned HTTP 400 with Only csv files are supported, while the complete uploaded content remained in a randomly named file in the configured temporary directory after the response. Relationship to prior public work I reviewed PR #5013, which migrated the Rich Text CSV upload path from…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
Apostrophe's cross-domain session migration emits a single-use session token as a URL query parameter and stores it in the Mongo cache. Any client that presents the token silently adopts the target user's full session (including passport.user) with no credentials. Details packages/apostrophe/modules/@apostrophecms/i18n/index.js: - Token generation on cross-host/locale flows (L479–487) and passport-bridge cross-locale redirect (passport-bridge/index.js L557–593) places the token in the URL. - acceptCrossDomainSessionToken (L258–298) reads ?aposCrossDomainSessionToken, Object.assigns the stored session (cookie + passport.user) onto req.session, then nulls the cache entry under namespace @apostrophecms/i18n:cross-domain-sessions. - Middleware without before: is registered for every URL (express findModuleMiddlewareAndRoutes L820–879); verified with APOSLOGALLROUTES=1. PoC Impact Session hijack / account takeover of the migrated user for anyone who obtains the token, inheriting…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
ApostropheCMS enforces per-type authorization on pages: a page type may declare editRole / publishRole (the core @apostrophecms/archive-page does), so a project can define restricted subtrees that only higher-privileged roles may create or edit within. The move() operation in the @apostrophecms/page module is supposed to enforce that a page may only be moved into a parent the actor has create rights over, the same boundary the page-insert route enforces. The destination-parent authorization check in move() is gated on the move being a cross-parent move (oldParent.id !== parent.id). A same-parent reorder (repositioning a page among its existing siblings under the same parent) short-circuits that check entirely, so an authenticated low-privileged editor who holds edit rights on one ordinary page nested under a restricted parent can reposition it and, in doing so, trigger an unguarded bulk database write that re-ranks the restricted parent's other children, documents the editor…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
@apostrophecms/db-connect ships an in-memory MongoDB emulation used by its SQLite, PostgreSQL, and multipostgres adapters. Its unsetNestedField helper walks an attacker-controlled dotted field path with no segment validation and calls delete on the resolved property. A low-privileged authenticated user (any role holding view-draft, which every editor and contributor has; reduced to any logged-in user when guestApiAccess is enabled) can send a piece-type REST GET request whose project query builder contains an exclusion key such as constructor.prototype.hasOwnProperty. On a SQLite or PostgreSQL adapter this key reaches unsetNestedField, which resolves Object.prototype and deletes a shared runtime method. Because that method is shared across the whole Node process, the deletion breaks request handling for every tenant and user until the process is restarted, giving a single request a persistent denial of service. Root Cause The projection query builder laundens only projection…
Recommended action: Upgrade to 4.33.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
ApostropheCMS enforces per-type authorization on pages: a page type may declare editRole / publishRole (and the core @apostrophecms/archive-page does), so a project can have page-type subtrees that only higher-privileged roles are allowed to create or edit within. The move() operation is supposed to enforce that a page may only be moved into a parent the actor has create rights over — this is the same boundary the page-insert route enforces (the insert target is fetched with .permission('create')). A regression in the move authorization guard silently disabled that destination check for every normal move. The guard now reads (oldParent.id !== parent.id) && (parent.type !== '@apostrophecms/archive-page') && (!parent.create) && (oldParent.type === '@apostrophecms/archive-page' && !parent.edit). Because the final && clause requires oldParent.type === '@apostrophecms/archive-page', the whole conjunction can only be true while restoring a page out of the archive. For any ordinary…
Recommended action: Upgrade to 4.32.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
The vulnerability is a single-request persistent DoS by submitting e.g. "PATCH /api/v1/article/ " with a valid editor session and body of {"toString.call":"x"}, overwriting the global toString function with value x. Fabian
Recommended action: Upgrade to 4.32.1 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
When prettyUrls: true is enabled on @apostrophecms/file (a documented SEO feature for serving uploaded files at clean URLs), the public pretty-URL handler builds the upstream URL using the raw Host HTTP request header: That URL is then fetch'ed and the response body + headers are streamed straight back to the requester. Because Host is fully attacker-controlled, an unauthenticated remote attacker can pivot the apostrophe process to issue outbound HTTP requests against any host it can reach on the private network. The path component is constrained to /uploads/attachments/<cuid-<slug.<ext (built from a local-DB lookup), which keeps the impact narrow: cross-instance data exfiltration is neutralised by cuid uniqueness, but blind-SSRF residuals remain (network-topology mapping via response-code / timing differences and verbose proxy/WAF 404 body disclosure). Verified on apostrophe@4.30.0 (latest); no fixed release exists. - Affected: apostrophe <= 4.30.0 when @apostrophecms/file…
Recommended action: Upgrade to 4.31.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
<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: 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.…
Recommended action: Upgrade to 4.31.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
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…
Recommended action: Upgrade to 4.29.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
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: This confirms: - Improper output encoding - Ability to escape <title / <meta contexts - Arbitrary script execution PoC…
Recommended action: Upgrade to 4.29.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.
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 --: A value like --x: red}</style<scriptalert(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…
Recommended action: Upgrade to 4.29.0 or later, then redeploy and verify.A vendor fix does not resolve your finding until you verify it in your own systems.