GitHub Security Advisory: GHSA-c276-fj82-f2pq
Package: apostrophe (NPM)
Severity: MODERATE (CVSS 5.3)
CWE: CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
Vulnerable versions: <= 4.28.0
Patched in: 4.29.0

Summary: ApostropheCMS: Information Disclosure via choices/counts Query Parameters Bypassing publicApiProjection Field Restrictions

Details:
## 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:

1. `getRestQuery` at piece-type/index.js:1120 calls `applyBuildersSafely(req.query)` (line 1122), which processes query parameters including `choices` and `counts` since both have `launder` methods (doc-type/index.js:2627-2628 and 2675-2676).

2. The `publicApiProjection` is applied afterward (line 1130-1134) as a MongoDB projection on the main query.

3. During query execution, the `choices` builder's `after` handler (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 `launder` method (line 2656)

   All schema field types (string, integer, float, select, boolean, date, slug, relationship) register query builders with `launder` methods via `addQueryBuilder` in `addFieldTypes.js`.

4. `toChoices` (line 2661) calls the field's `choices` function, which typically calls `sortedDistinct` → `toDistinct`. The `toDistinct` method (doc-type/index.js:2811) executes `db.distinct(property, criteria)` — a MongoDB operation that returns all distinct values for the given property matching the criteria. **MongoDB's `distinct` operation does not respect projections**; it operates directly on the specified field regardless of any projection set on the query.

5. 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 against `publicApiProjection` or `removeForbiddenFields`.

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

```bash
# 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