CVE-2026-39857 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.
Information Disclosure via `choices`/`counts` Query Parameters Bypassing publicApiProjection Field Restrictions
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 CVE-2026-39857.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
CVE-2026-39857 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 378,156 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
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.
apostrophenpmDescription
Summary
The choices and counts query parameters in the Apostrophe CMS REST API allow unauthenticated users to extract distinct field values for any schema field that has a registered query builder, completely bypassing publicApiProjection restrictions that are intended to limit which fields are exposed publicly. Fields protected by viewPermission are similarly exposed.
Details
When a piece type configures publicApiProjection to enable public API access while restricting visible fields, the restriction is enforced via a MongoDB projection on the main query (piece-type/index.js:1130-1134). However, the choices and counts query builders bypass this protection through a separate code path.
The vulnerable flow:
-
getRestQueryat piece-type/index.js:1120 callsapplyBuildersSafely(req.query)(line 1122), which processes query parameters includingchoicesandcountssince both havelaundermethods (doc-type/index.js:2627-2628 and 2675-2676). -
The
publicApiProjectionis applied afterward (line 1130-1134) as a MongoDB projection on the main query. -
During query execution, the
choicesbuilder'safterhandler (doc-type/index.js:2636-2668) iterates over requested field names. The only validation is:- The field has a registered builder (
_.has(query.builders, filter)at line 2651) - The builder has a
laundermethod (line 2656)
All schema field types (string, integer, float, select, boolean, date, slug, relationship) register query builders with
laundermethods viaaddQueryBuilderinaddFieldTypes.js. - The field has a registered builder (
-
toChoices(line 2661) calls the field'schoicesfunction, which typically callssortedDistinct→toDistinct. ThetoDistinctmethod (doc-type/index.js:2811) executesdb.distinct(property, criteria)— a MongoDB operation that returns all distinct values for the given property matching the criteria. MongoDB'sdistinctoperation does not respect projections; it operates directly on the specified field regardless of any projection set on the query. -
The results are stored via
query.set('choicesResults', choices)(line 2666) and returned directly in the API response at piece-type/index.js:292-296 without any filtering againstpublicApiProjectionorremoveForbiddenFields.
The same bypass applies to viewPermission-protected fields: removeForbiddenFields (doc-type/index.js:1585-1611) only processes document results from toArray(), not the separate choices/counts data.
The page REST API has the same issue at page/index.js:371-376.
PoC
# Prerequisites:
# - An Apostrophe 4.x instance with a piece type configured with publicApiProjection
# - Example: an 'article' piece type with:
# publicApiProjection: { title: 1, slug: 1, _url: 1 }
# and additional schema fields like 'status' (select), 'priority' (integer),
# or 'internalNotes' (string) NOT in the projection
# 1. Verify normal API access only returns projected fields
curl -s 'http://localhost:3000/api/v1/article' | python3 -m json.tool
# Response results contain only: title, slug, _url (as configured)
# 2. Extract distinct values of a non-projected field via choices
curl -s 'http://localhost:3000/api/v1/article?choices=status' | python3 -m json.tool
# Response includes:
# "choices": {"status": [{"value": "draft", "label": "draft"}, {"value": "published", "label": "published"}, ...]}
# 3. Extract distinct values with document counts via counts
curl -s 'http://localhost:3000/api/v1/article?counts=priority' | python3 -m json.tool
# Response includes:
# "counts": {"priority": [{"value": 1, "label": "1", "count": 15}, {"value": 2, "label": "2", "count": 8}, ...]}
# 4. Multiple fields can be extracted at once
curl -s 'http://localhost:3000/api/v1/article?choices=status,priority,internalNotes'
Impact
- Distinct field values leaked: An unauthenticated attacker can extract all distinct values of any schema field on any piece type that has
publicApiProjectionconfigured, even when those fields are explicitly excluded from the projection. - Field types affected: All field types that register query builders: string, slug, integer, float, select, boolean, date, and relationship fields.
- Count disclosure: The
countsvariant additionally reveals how many documents have each distinct value, providing statistical information about the dataset. - viewPermission bypass: Fields protected with
viewPermission(intended for role-based field access) are also exposed via this path. - Both APIs affected: The piece-type REST API (piece-type/index.js:292-296) and page REST API (page/index.js:371-376) are both vulnerable.
- Real-world impact: If a CMS stores sensitive data in schema fields (e.g., internal status values, priority levels, internal categories, user-facing content marked as restricted), all distinct values are extractable by any unauthenticated visitor.
Recommended Fix
In the choices builder's after handler (doc-type/index.js:2636-2668), add validation to skip fields not permitted by publicApiProjection and viewPermission:
// doc-type/index.js, in the choices builder's after handler (line 2644 area)
for (const filter of filters) {
if (!_.has(query.builders, filter)) {
continue;
}
if (!query.builders[filter].launder) {
continue;
}
// NEW: Enforce publicApiProjection restrictions on choices/counts
const publicApiProjection = query.get('project');
if (publicApiProjection && !publicApiProjection[filter]) {
continue;
}
// NEW: Enforce viewPermission field restrictions
const field = self.schema.find(f => f.name === filter);
if (field && field.viewPermission &&
!self.apos.permission.can(query.req, field.viewPermission.action, field.viewPermission.type)) {
continue;
}
const _query = baseQuery.clone();
_query[filter](null);
choices[filter] = await _query.toChoices(filter, { counts: query.get('counts') });
}
Additionally, apply the same fix in the page REST API handler (page/index.js) for consistency.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | apostrophe | all versions | 4.29.0npm install apostrophe@4.29.0 |
Detection & mitigation playbook
Open-source dependencyDetect
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.
Fix
Update apostrophe to 4.29.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-39857 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-39857 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-39857. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-39857 in your dependencies?
O3 Security finds CVE-2026-39857 across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.