Skip to content

Incomplete fix for CVE-2026-39857 — publicApiProjection choices/counts guard bypassable via relationship slug-builder aliases

Moderate
boutell published GHSA-xmpp-f9v3-r7qh Aug 12, 2026

Package

npm apostrophe (npm)

Affected versions

>= 4.29.0

Patched versions

>=4.32.0

Description

Summary

The fix for CVE-2026-39857 added choicesFieldAllowedByProjection() in @apostrophecms/doc-type to stop the ?choices= / ?counts= query parameters from leaking distinct values of fields excluded from publicApiProjection. The guard resolves the schema field via an EXACT-NAME match (self.schema.find(f => f.name === filter)) and, when no field matches, returns true (allowed). Query builders whose registered name differs from the schema field name — specifically the relationship "slug" alias builders (author / authorAnd for a relationship field named _author) — are therefore not gated. An unauthenticated user can still extract distinct relationship choices for a relationship field that was deliberately excluded from publicApiProjection. The advisory for CVE-2026-39857 explicitly lists "relationship" among the field types meant to be protected, so this is a residual of the same issue.

Details

For a relationship field _author, Apostrophe registers (schema/lib/addFieldTypes.js → schema/index.js addRelationshipSlugQueryBuilder, line ~1509) extra builders named without the leading underscore: author and authorAnd. Each has a launder method and a choices function (relationshipQueryBuilderChoices).

In the choices after handler (doc-type/index.js ~2769-2794) a requested filter is honored if:

  1. _.has(query.builders, filter) — true for author
  2. query.builders[filter].launder — true for author
  3. choicesFieldAllowedByProjection(filter, publicApiProjection) — the guard

The guard (doc-type/index.js 1634-1656):

choicesFieldAllowedByProjection(filter, projection) {
  if (!projection || !Object.keys(projection).length) return true;
  const field = self.schema.find(f => f.name === filter); // exact name
  if (!field) return true;                                 // <-- fails open
  const topLevel = filter.split('.')[0];
  ...
}

Because the schema field is named _author but the builder/filter is author, schema.find returns undefined and the guard returns true, so toChoices('author') runs. relationshipQueryBuilderChoices executes query.toDistinct(field.idsStorage) on the host collection (MongoDB distinct ignores projections — the same root cause as the parent CVE) to obtain referenced related-doc ids, then queries the related type. ?counts=author is equivalent (the counts builder delegates to choices). Page REST API shares the same code path.

Scope/limitation (honest): the related-type lookup uses the anonymous request, so only PUBLICLY-VISIBLE related docs are returned (title/slug). The residual leak is therefore the relationship linkage — which public related docs are referenced by the host pieces — that the operator removed from publicApiProjection. It does not expose non-public related docs or arbitrary excluded scalar host fields.

PoC

Prereqs: Apostrophe >= 4.29.0; a piece type (e.g. article) with:

  • publicApiProjection: { title: 1, slug: 1, _url: 1 }
  • a relationship field _author (related type publicly viewable) NOT in the projection
# Baseline: relationship is not exposed
curl -s 'http://localhost:3000/api/v1/article' | python3 -m json.tool
# results contain only title, slug, _url

# Bypass via the relationship slug-builder alias (note: "author", not "_author")
curl -s 'http://localhost:3000/api/v1/article?choices=author' | python3 -m json.tool
# -> "choices": { "author": [ { "label": "<author title>", "value": "<author slug>" }, ... ] }

# counts variant additionally reveals reference counts
curl -s 'http://localhost:3000/api/v1/article?counts=author'

By contrast ?choices=_author and ?choices=<excluded-scalar> are correctly blocked by the fix — only the alias name leaks. (Verified: the exact guard executed in isolation returns false for secret/_author and true for author/authorAnd; a faithful simulation of the after handler shows author/authorAnd reach toChoices() while secret/_author are blocked.)

Impact

Unauthenticated information disclosure of relationship linkage (set of referenced, publicly-visible related docs by title/slug, plus per-value counts via ?counts=) for relationship fields that an operator intentionally excluded from publicApiProjection. This is the relationship-field portion of CVE-2026-39857's intended protection, left unguarded by the exact-name match. Lower severity than the parent CVE (no arbitrary scalar field values, no non-public docs).

Suggested fix

Resolve the schema field by the builder's underlying field, not the literal filter string. Either map the alias back to the relationship field before the projection check, or gate on the field actually queried. Concretely, in choicesFieldAllowedByProjection, also match relationship alias/idsStorage builders, e.g. resolve filter (and its And suffix) to the relationship field whose name.replace(/^_/, '') equals the filter, and apply the projection check against that relationship field (and its idsStorage). Equivalently, compute the field the builder distinct-queries and require that property to be permitted by publicApiProjection.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

CVE ID

CVE-2026-63668

Weaknesses

No CWEs

Credits