Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
MEDIUM severity

ApostropheCMS: Unauthenticated publicApiProjection bypass via search and sort query builders in the public REST APIGHSA-rqgm-cfrp-4hx8

MEDIUMFix: apostrophecms/apostrophe@06e4b37

GHSA-rqgm-cfrp-4hx8 is a medium-severity (CVSS 5.3) Information Exposure vulnerability in apostrophe. A fix is available for apostrophe — see the affected versions and patch details below.

Also known asCVE-2026-112954
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for GHSA-rqgm-cfrp-4hx8.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs22th percentile — riskier than 22% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

GHSA-rqgm-cfrp-4hx8 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

8other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
apostrophenpm
18Kdownloads / week

Description

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:

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):

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):

# [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)ObservedVerdict
GET /api/v1/article_id,title,slug,type,metaType,cacheInvalidatedAtprojection works on direct read
?search=zzsecretalphaonly Article A (with textScore)leak: locates doc by hidden field
?search=zznotpresentxyz0 resultsoracle confirmed
?sort[secret]=1B, D, A, E, C (= 10,30,50,70,90)leak: full hidden ordering
?sort[secret]=-1C, E, A, D, Breverse confirmed
?project[secretNote]=1secretNote absentgated (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.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmapostropheall versions4.34.0npm install apostrophe@4.34.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for apostrophe, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update apostrophe to 4.34.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-rqgm-cfrp-4hx8 is resolved across your whole dependency graph.

  3. Workarounds

    Assume what was exposed is already known: rotate any credential, token or key that the affected component could return, restrict the endpoint to callers that genuinely need it, and strip sensitive fields from responses and error output at the boundary rather than relying on the client not to read them.

Frequently Asked Questions

### 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 f
O3 Security · Impact-Aware SCA

Is GHSA-rqgm-cfrp-4hx8 in your dependencies?

Find it across npm, including transitive dependencies.

ApostropheCMS: Unauthenticated publicApiProjection…