{"id":"GHSA-rqgm-cfrp-4hx8","revision":null,"records":[{"revision":"f47b39e8bddd4947b36efac799656640","observedAt":"2026-10-23T16:01:20.601+00:00","statement":{"source":"cve-program","id":"CVE-2026-112954","aliases":[],"title":"ApostropheCMS: Unauthenticated publicApiProjection bypass via search and sort query builders in the public REST API","description":"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 field information through search membership and sort ordering oracles. Returned fields remain filtered by the projection, so the issue permits targeted inference about hidden values rather than an unrestricted field dump. This issue is fixed in version 4.34.0.","state":0,"publishedAt":"2026-10-20T15:51:01.621+00:00","modifiedAt":"2026-10-23T01:50:55.523+00:00","affected":[{"ecosystem":"Vendor product","name":"apostrophe","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"apostrophecms\",\n                    \"product\": \"apostrophe\",\n                    \"versions\": [\n                        {\n                            \"version\": \"< 4.34.0\",\n                            \"status\": \"affected\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","score":5.3,"severity":"MEDIUM"}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-rqgm-cfrp-4hx8","https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498","https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea","https://github.com/apostrophecms/apostrophe/releases/tag/apostrophe@4.34.0"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-112954","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-112954","identifiers":["CVE-2026-112954"]},"retained":false,"historicalAffected":[]},{"revision":"0333644028cd419d8fb05fac4a55f29d","observedAt":"2026-11-07T04:33:53.58+00:00","statement":{"source":"euvd","id":"EUVD-2026-105104","aliases":["CVE-2026-112954","GHSA-rqgm-cfrp-4hx8"],"title":"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 field information through search membership and sort orderin…","description":"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 field information through search membership and sort ordering oracles. Returned fields remain filtered by the projection, so the issue permits targeted inference about hidden values rather than an unrestricted field dump. This issue is fixed in version 4.34.0.","state":0,"publishedAt":null,"modifiedAt":null,"affected":[],"metrics":[{"system":"EUVD CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","score":5.3,"severity":null}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-rqgm-cfrp-4hx8","https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498","https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea","https://github.com/apostrophecms/apostrophe/releases/tag/apostrophe@4.34.0"],"sourceUrl":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-105104","license":"ENISA EUVD · source terms and attribution apply","enrichment":{"sourceStatus":null,"weaknesses":[],"productCriteria":[],"sourceDates":{"datePublished":"Oct 20, 2026, 3:51:01 PM","dateUpdated":"Oct 23, 2026, 1:50:55 AM"},"relatedAdvisories":[],"note":"EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON."},"key":"euvd:EUVD-2026-105104","identifiers":["EUVD-2026-105104","CVE-2026-112954","GHSA-rqgm-cfrp-4hx8"]},"retained":false,"historicalAffected":[]},{"revision":"7632f2bb050a4d62a21d0074ada70ecd","observedAt":"2026-10-25T05:17:18.597+00:00","statement":{"source":"nvd","id":"CVE-2026-112954","aliases":[],"title":"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 field information through search membership and sort orderin…","description":"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 field information through search membership and sort ordering oracles. Returned fields remain filtered by the projection, so the issue permits targeted inference about hidden values rather than an unrestricted field dump. This issue is fixed in version 4.34.0.","state":0,"publishedAt":"2026-10-20T16:17:06.367+00:00","modifiedAt":"2026-10-23T02:16:55.033+00:00","affected":[],"metrics":[{"system":"CVSS 3.1 · security-advisories@github.com · Secondary","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","score":5.3,"severity":"MEDIUM"}],"references":["https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498","https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea","https://github.com/apostrophecms/apostrophe/releases/tag/apostrophe@4.34.0","https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-rqgm-cfrp-4hx8"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-112954","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-200"],"productCriteria":[],"sourceDates":{"published":"2026-10-20T16:17:06.367","lastModified":"2026-10-23T02:16:55.033"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-112954","identifiers":["CVE-2026-112954"]},"retained":false,"historicalAffected":[]},{"revision":"d9949152dac744c889dfd970a19be7ce","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-rqgm-cfrp-4hx8","aliases":["CVE-2026-112954"],"title":"ApostropheCMS: Unauthenticated publicApiProjection bypass via search and sort query builders in the public REST API","description":"### Summary\nApostropheCMS 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:\n\n- 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\n- use `?sort[hiddenField]=1` as an **ordering oracle** to rank documents by a hidden field's value.\n\nThis 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`.\n\n### Details\nFor an anonymous caller, `getRestQuery` (piece types: `modules/@apostrophecms/piece-type/index.js:1142`; pages: `modules/@apostrophecms/page/index.js`) does:\n\n```js\nconst query = self.find(req).attachments(true);\nquery.applyBuildersSafely(req.query);            // (1) apply ALL launderable builders from the query string\n...\nquery.project({ ...self.options.publicApiProjection, cacheInvalidatedAt: 1 }); // (2) force read projection\nquery.set('publicApiProjection', self.options.publicApiProjection);            // (3) used only by the choices/counts gate\n```\n\n`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.\n\n**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.\n\n**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.\n\nNeither builder consults `publicApiProjection`, whereas `choices`/`counts` are gated at `doc-type/index.js:2807` via `choicesFieldAllowedByProjection`.\n\n### PoC\n**Preconditions:** the piece/page type sets `publicApiProjection`, and its schema has at least one searchable (default) or sortable field not included in the projection.\n\nTest type: `article` exposes only `title`, `slug`, `_id`, but has hidden `secret` (integer) and `secretNote` (searchable string):\n\n```js\nexport default {\n  extend: '@apostrophecms/piece-type',\n  options: { publicApiProjection: { title: 1, slug: 1, _id: 1 } },\n  fields: { add: {\n    secret:     { type: 'integer' },\n    secretNote: { type: 'string' }   // searchable by default; NOT in the projection\n  } }\n};\n```\n\nFive articles were inserted and published via the authenticated REST API:\n`A.secret=50, secretNote=\"zzsecretalpha\"`; `B=10/zzsecretbravo`; `C=90/zzsecretcharlie`; `D=30/zzsecretdelta`; `E=70/zzsecretecho`.\nThe insert responses confirm the hidden `secretNote` value is written into the search index, e.g. for A: `\"highSearchText\": \"... zzsecretalpha\"`, `\"lowSearchText\": \"... zzsecretalpha\"`.\n\nAnonymous requests (no credentials, no session):\n\n```bash\n# [1] Baseline: projection hides secret/secretNote on direct read\ncurl -s 'http://localhost:3000/api/v1/article?perPage=1'\n\n# [2] Search oracle: value exists ONLY in hidden secretNote\ncurl -s 'http://localhost:3000/api/v1/article?search=zzsecretalpha'\ncurl -s 'http://localhost:3000/api/v1/article?search=zznotpresentxyz'\n\n# [3] Sort oracle: order by hidden integer field\ncurl -s 'http://localhost:3000/api/v1/article?sort%5Bsecret%5D=1'\ncurl -s 'http://localhost:3000/api/v1/article?sort%5Bsecret%5D=-1'\n\n# Negative control: projection override is blocked\ncurl -s 'http://localhost:3000/api/v1/article?project%5BsecretNote%5D=1&perPage=1'\n```\n\nResults (verified on ApostropheCMS 4.32.2, MongoDB 7.0):\n\n| Request (anonymous)       | Observed                                          | Verdict                               |\n| ------------------------- | ------------------------------------------------- | ------------------------------------- |\n| `GET /api/v1/article`     | `_id,title,slug,type,metaType,cacheInvalidatedAt` | projection works on direct read       |\n| `?search=zzsecretalpha`   | only Article A (with textScore)                   | **leak**: locates doc by hidden field |\n| `?search=zznotpresentxyz` | 0 results                                         | oracle confirmed                      |\n| `?sort[secret]=1`         | B, D, A, E, C  (= 10,30,50,70,90)                 | **leak**: full hidden ordering        |\n| `?sort[secret]=-1`        | C, E, A, D, B                                     | reverse confirmed                     |\n| `?project[secretNote]=1`  | secretNote absent                                 | gated (projection override blocked)   |\n\n### Impact\nUnauthenticated, network-reachable partial disclosure of fields the developer explicitly restricted with `publicApiProjection`:\n\n- **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.\n- **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.\n\nNo 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`).\n\n### Remediation\nGate `search`, `sort` (and `autocomplete`, which feeds `search`) on the public API path the same way `choices`/`counts` are gated. When `publicApiProjection` is in force:\n\n- **sort:** reject or drop any sort key not permitted by the projection, reusing `choicesFieldAllowedByProjection`.\n- **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.","state":0,"publishedAt":"2026-10-21T17:12:15+00:00","modifiedAt":"2026-10-21T17:15:08.6408059+00:00","affected":[{"ecosystem":"npm","name":"apostrophe","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"4.34.0"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"MODERATE"}],"references":["https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-rqgm-cfrp-4hx8","https://nvd.nist.gov/vuln/detail/CVE-2026-112954","https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498","https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea","https://github.com/apostrophecms/apostrophe","https://github.com/apostrophecms/apostrophe/releases/tag/apostrophe@4.34.0"],"sourceUrl":"https://github.com/advisories/GHSA-rqgm-cfrp-4hx8","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-rqgm-cfrp-4hx8","identifiers":["GHSA-rqgm-cfrp-4hx8","CVE-2026-112954"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-08T06:06:40.8743578+00:00","complete":true,"records":1769,"lastAttemptAt":"2026-11-08T06:06:40.8743578+00:00","lastSuccessAt":"2026-11-08T06:06:41.0239033+00:00","error":null,"lastVerifiedAt":"2026-11-08T06:06:41.0239043+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-eaa64dac63caa79c8d4905619258c41c51b39e461b996124e1a21b5e85e7e738-83dbcd705162368a7389ef21c7d5877b90bc9ecd8679671f4b5e02683a8e1d4f","complete":true,"records":421074,"lastAttemptAt":"2026-11-08T06:06:40.5628876+00:00","lastSuccessAt":"2026-11-08T06:06:40.5495855+00:00","error":null,"lastVerifiedAt":"2026-11-08T06:06:40.5628876+00:00","snapshotPublishedAt":"2026-11-08T05:23:47+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-ce0df89a405747cda5b7c61ae0b48fb2","complete":true,"records":13633,"lastAttemptAt":"2026-11-08T06:07:06.8623217+00:00","lastSuccessAt":"2026-11-08T05:08:40.263+00:00","error":null,"lastVerifiedAt":"2026-11-08T05:08:40.263+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13685,"expectedIdentifiers":13685,"failedIdentifiers":0},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-08T06:06:40.8743578+00:00","complete":true,"records":1096,"lastAttemptAt":"2026-11-08T06:06:40.8743578+00:00","lastSuccessAt":"2026-11-08T06:06:41.0246874+00:00","error":null,"lastVerifiedAt":"2026-11-08T06:06:41.0246876+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-f61a222d691b4992bb1403f1a88575db","complete":true,"records":13645,"lastAttemptAt":"2026-11-08T06:07:06.2347163+00:00","lastSuccessAt":"2026-11-08T06:07:06.231+00:00","error":null,"lastVerifiedAt":"2026-11-08T06:07:06.231+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13685,"expectedIdentifiers":13685,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-0eb4d6dde62dffacd040ad992d4fd5a65cd7d0dc5f5f4cfa8b41967fc6f9ca5f","complete":true,"records":9914,"lastAttemptAt":"2026-11-08T06:06:40.830411+00:00","lastSuccessAt":"2026-11-06T20:10:36.6450506+00:00","error":null,"lastVerifiedAt":"2026-11-08T06:06:40.830411+00:00","snapshotPublishedAt":"2026-11-06T20:02:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-fde804134ecdda0ddd5ff5c26ee1fff5a6fa659311f7ef2e51c6ad54964db0e2","complete":true,"records":1971,"lastAttemptAt":"2026-11-08T06:06:40.6194681+00:00","lastSuccessAt":"2026-11-06T19:03:17.0399903+00:00","error":null,"lastVerifiedAt":"2026-11-08T06:06:40.6194681+00:00","snapshotPublishedAt":"2026-11-06T18:17:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-335633e78a15c9cde10a6025895b5617717087fb945489ae7e15d377bacf34a9","complete":true,"records":3055,"lastAttemptAt":"2026-11-08T06:06:40.8424247+00:00","lastSuccessAt":"2026-11-06T16:03:00.7615789+00:00","error":null,"lastVerifiedAt":"2026-11-08T06:06:40.8424247+00:00","snapshotPublishedAt":"2026-11-06T16:02:48+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-40ad6183a951565ace1520a9b4d631e1d0bfb1d4bf7c5574c375032559f3ff53","complete":true,"records":231292,"lastAttemptAt":"2026-11-08T06:06:40.7260451+00:00","lastSuccessAt":"2026-11-07T10:29:00.4821259+00:00","error":null,"lastVerifiedAt":"2026-11-08T06:06:40.7260451+00:00","snapshotPublishedAt":"2026-11-07T09:32:47+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}