TL;DR
- What: In ApostropheCMS, the REST route
GET /api/v1/<type>/:_id/locales(pieces and pages) did not callpublicApiCheckAsync(), the check its sibling read routes use to refuse callers when a type's REST API is not public. In my local test on 4.32.2, an anonymous caller got 404 from the normal read routes for a private piece and for the home page, and 200 with locale data from:_id/localesfor both. - Impact: per the advisory, an unauthenticated caller who knows a document's
_idcould learn that the document exists and which locales it is published in. Rated Moderate 5.3 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N). CWE-200 and CWE-862. - Fixed in 4.33.0. Advisory GHSA-gqh3-7856-rjjg and the release notes credit two reporters: me and @thotamurari. CVE-2026-113668.
Why you should care
ApostropheCMS is a Node.js CMS with a REST API for its pages and "piece" types (blog posts, products, anything you define). By design, that API is closed to the public unless a type opts in with a publicApiProjection. The check that enforces this is publicApiCheckAsync().
The leak is small: no document content, only whether an _id exists and which locales it has. But it is a document-existence answer the CMS had decided not to give anonymous callers, and on multi-locale sites the locale list says which markets a document was prepared for.
The setup
In packages/apostrophe/modules/@apostrophecms/piece-type/index.js at apostrophe@4.32.2, the sibling read routes call the check first. getAll, line 259:
await self.publicApiCheckAsync(req); and getOne does the same at line 319. For a type with no public projection, an anonymous caller gets 404.
The bug
The locales route in the same file, lines 397-402, has no such call:
':_id/locales': async (req) => { const _id = self.inferIdLocaleAndMode(req, req.params._id); return { results: await self.apos.doc.getLocales(req, _id) }; } The page module has the same route without the check, at page/index.js line 925. getLocales() in the doc module looks up every locale version of the _id, filtering only to published versions when the caller can't view drafts, and returns _id and aposLocale for each. Nothing on that path asks whether this caller may use the REST API for this type at all.
The "aha"
The route sat in the same file as the routes that call the check, and it was the one that didn't.
Proof of concept (benign)
I installed apostrophe@4.32.2 from npm and ran it locally with an in-process MongoDB (mongodb-memory-server), synthetic content only. One custom piece type, topic, with no publicApiProjection, so its REST API is meant to be private. One published topic, created by an admin account. The anonymous caller sent no cookies at all.
These are the recorded results:
# anonymous caller, piece type "topic" GET /api/v1/topic -> 404 notfound GET /api/v1/topic/<id> -> 404 notfound GET /api/v1/topic/<id>/locales -> 200 {"results":[{"_id":"<id>:en:published","aposLocale":"en:published"}]} # anonymous caller, home page GET /api/v1/@apostrophecms/page/<homeId> -> 404 notfound GET /api/v1/@apostrophecms/page/<homeId>/locales -> 200 {"results":[{"_id":"<homeId>:en:published","aposLocale":"en:published"}]} As a control, I added await self.publicApiCheckAsync(req); to both locales handlers and re-ran: the anonymous requests returned 404, and the admin's request still returned 200.
Honest boundary: the test site had one locale, so the leak reduced to "this _id exists". I did not test a multi-locale site. The home page _id is visible to any visitor, while the piece _id in my test came from the admin's own listing, so in practice the caller needs some way to learn an _id. I did not re-run the test on 4.33.0.
The fix
ApostropheCMS fixed it in 4.33.0, published to npm on 2026-09-30, in commit f84fd41, "Merge all draft security advisories (#5631)". The route now runs the same check, at apostrophe@4.33.0, lines 404-406:
':_id/locales': async (req) => { const _id = self.inferIdLocaleAndMode(req, req.params._id); await self.publicApiCheckAsync(req); and the page route has the same line. The release notes say the fix also went further than my report: apos.doc.getLocales() now applies document-level view permissions, so even a logged-in caller only sees the locale versions they are allowed to view, including draft locales of types restricted by viewRole. The notes thank both reporters.
Takeaways
- If every route must start with the same check, make forgetting it hard. Here it was a line each handler had to remember. A router-level hook, or a test that walks the module's routes and calls each one anonymously, catches the one that doesn't.
- Metadata endpoints need the same gate as data endpoints. "Which locales exist" sounds harmless, but it answers "does this document exist" for a caller who was meant to get 404.
- Check the helper too. The maintainers fixed the route and also made
getLocales()check view permissions, so the next caller of that helper is safe by default.
Disclosure timeline (UTC)
- 2026-09-05: reproduced locally on
apostrophe@4.32.2(synthetic data). - 2026-09-08: reported through GitHub's private vulnerability reporting.
- 2026-09-30: 4.33.0 published with the fix.
- 2026-10-02: advisory GHSA-gqh3-7856-rjjg published by the maintainers.
- 2026-10-20: CVE-2026-113668 published to the CVE List.
Credit / CTA
If you run ApostropheCMS, upgrade to 4.33.0 or later (the latest is 4.34.0).
If you maintain an API where each route checks access for itself, try this: list every route in a module and call each one with no session. Any route that answers when its siblings say 404 is worth a look.
The advisory credits two reporters: me and @thotamurari. Comments and corrections welcome.
References
- ApostropheCMS security advisory GHSA-gqh3-7856-rjjg, published 2026-10-02. https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-gqh3-7856-rjjg
- CVE-2026-113668. https://www.cve.org/CVERecord?id=CVE-2026-113668
- Fix commit
f84fd41, "Merge all draft security advisories (#5631)". https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350 -
piece-type/index.jsatapostrophe@4.32.2(before the fix). https://github.com/apostrophecms/apostrophe/blob/apostrophe@4.32.2/packages/apostrophe/modules/@apostrophecms/piece-type/index.js -
piece-type/index.jsatapostrophe@4.33.0(after the fix). https://github.com/apostrophecms/apostrophe/blob/apostrophe@4.33.0/packages/apostrophe/modules/@apostrophecms/piece-type/index.js - MITRE CWE-862, Missing Authorization. https://cwe.mitre.org/data/definitions/862.html
Santosh Kumar Puppala — AI/ML Platform Architect and security researcher (multiple CVEs; creator of Norviq). GitHub: @Santoshkumarpuppala
