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:
_.has(query.builders, filter) — true for author
query.builders[filter].launder — true for author
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.
Summary
The fix for CVE-2026-39857 added
choicesFieldAllowedByProjection()in@apostrophecms/doc-typeto stop the?choices=/?counts=query parameters from leaking distinct values of fields excluded frompublicApiProjection. The guard resolves the schema field via an EXACT-NAME match (self.schema.find(f => f.name === filter)) and, when no field matches, returnstrue(allowed). Query builders whose registered name differs from the schema field name — specifically the relationship "slug" alias builders (author/authorAndfor 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 frompublicApiProjection. 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.jsaddRelationshipSlugQueryBuilder, line ~1509) extra builders named without the leading underscore:authorandauthorAnd. Each has alaundermethod and achoicesfunction (relationshipQueryBuilderChoices).In the choices
afterhandler (doc-type/index.js ~2769-2794) a requested filter is honored if:_.has(query.builders, filter)— true forauthorquery.builders[filter].launder— true forauthorchoicesFieldAllowedByProjection(filter, publicApiProjection)— the guardThe guard (doc-type/index.js 1634-1656):
Because the schema field is named
_authorbut the builder/filter isauthor,schema.findreturnsundefinedand the guard returnstrue, sotoChoices('author')runs.relationshipQueryBuilderChoicesexecutesquery.toDistinct(field.idsStorage)on the host collection (MongoDBdistinctignores projections — the same root cause as the parent CVE) to obtain referenced related-doc ids, then queries the related type.?counts=authoris equivalent (thecountsbuilder delegates tochoices). 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 }_author(related type publicly viewable) NOT in the projectionBy contrast
?choices=_authorand?choices=<excluded-scalar>are correctly blocked by the fix — only the alias name leaks. (Verified: the exact guard executed in isolation returns false forsecret/_authorand true forauthor/authorAnd; a faithful simulation of theafterhandler showsauthor/authorAndreachtoChoices()whilesecret/_authorare 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 frompublicApiProjection. 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/idsStoragebuilders, e.g. resolvefilter(and itsAndsuffix) to the relationship field whosename.replace(/^_/, '')equals the filter, and apply the projection check against that relationship field (and itsidsStorage). Equivalently, compute the field the builder distinct-queries and require that property to be permitted bypublicApiProjection.