GHSA-C276-FJ82-F2PQ

Vulnerability from github – Published: 2026-04-16 20:45 – Updated: 2026-04-16 20:45
VLAI?
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:

  4. The field has a registered builder (_.has(query.builders, filter) at line 2651)
  5. 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.

  1. toChoices (line 2661) calls the field's choices function, which typically calls sortedDistincttoDistinct. 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.

  2. 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

# 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 publicApiProjection configured, 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 counts variant 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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.28.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "apostrophe"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.29.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-39857"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-200"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-16T20:45:15Z",
    "nvd_published_at": "2026-04-15T20:16:36Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe `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.\n\n## Details\n\nWhen 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.\n\nThe vulnerable flow:\n\n1. `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).\n\n2. The `publicApiProjection` is applied afterward (line 1130-1134) as a MongoDB projection on the main query.\n\n3. During query execution, the `choices` builder\u0027s `after` handler (doc-type/index.js:2636-2668) iterates over requested field names. The only validation is:\n   - The field has a registered builder (`_.has(query.builders, filter)` at line 2651)\n   - The builder has a `launder` method (line 2656)\n\n   All schema field types (string, integer, float, select, boolean, date, slug, relationship) register query builders with `launder` methods via `addQueryBuilder` in `addFieldTypes.js`.\n\n4. `toChoices` (line 2661) calls the field\u0027s `choices` function, which typically calls `sortedDistinct` \u2192 `toDistinct`. The `toDistinct` method (doc-type/index.js:2811) executes `db.distinct(property, criteria)` \u2014 a MongoDB operation that returns all distinct values for the given property matching the criteria. **MongoDB\u0027s `distinct` operation does not respect projections**; it operates directly on the specified field regardless of any projection set on the query.\n\n5. The results are stored via `query.set(\u0027choicesResults\u0027, choices)` (line 2666) and returned directly in the API response at piece-type/index.js:292-296 without any filtering against `publicApiProjection` or `removeForbiddenFields`.\n\nThe 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.\n\nThe page REST API has the same issue at page/index.js:371-376.\n\n## PoC\n\n```bash\n# Prerequisites:\n# - An Apostrophe 4.x instance with a piece type configured with publicApiProjection\n# - Example: an \u0027article\u0027 piece type with:\n#   publicApiProjection: { title: 1, slug: 1, _url: 1 }\n#   and additional schema fields like \u0027status\u0027 (select), \u0027priority\u0027 (integer),\n#   or \u0027internalNotes\u0027 (string) NOT in the projection\n\n# 1. Verify normal API access only returns projected fields\ncurl -s \u0027http://localhost:3000/api/v1/article\u0027 | python3 -m json.tool\n# Response results contain only: title, slug, _url (as configured)\n\n# 2. Extract distinct values of a non-projected field via choices\ncurl -s \u0027http://localhost:3000/api/v1/article?choices=status\u0027 | python3 -m json.tool\n# Response includes:\n# \"choices\": {\"status\": [{\"value\": \"draft\", \"label\": \"draft\"}, {\"value\": \"published\", \"label\": \"published\"}, ...]}\n\n# 3. Extract distinct values with document counts via counts\ncurl -s \u0027http://localhost:3000/api/v1/article?counts=priority\u0027 | python3 -m json.tool\n# Response includes:\n# \"counts\": {\"priority\": [{\"value\": 1, \"label\": \"1\", \"count\": 15}, {\"value\": 2, \"label\": \"2\", \"count\": 8}, ...]}\n\n# 4. Multiple fields can be extracted at once\ncurl -s \u0027http://localhost:3000/api/v1/article?choices=status,priority,internalNotes\u0027\n```\n\n## Impact\n\n- **Distinct field values leaked**: An unauthenticated attacker can extract all distinct values of any schema field on any piece type that has `publicApiProjection` configured, even when those fields are explicitly excluded from the projection.\n- **Field types affected**: All field types that register query builders: string, slug, integer, float, select, boolean, date, and relationship fields.\n- **Count disclosure**: The `counts` variant additionally reveals how many documents have each distinct value, providing statistical information about the dataset.\n- **viewPermission bypass**: Fields protected with `viewPermission` (intended for role-based field access) are also exposed via this path.\n- **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.\n- **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.\n\n## Recommended Fix\n\nIn the `choices` builder\u0027s `after` handler (doc-type/index.js:2636-2668), add validation to skip fields not permitted by `publicApiProjection` and `viewPermission`:\n\n```javascript\n// doc-type/index.js, in the choices builder\u0027s after handler (line 2644 area)\nfor (const filter of filters) {\n  if (!_.has(query.builders, filter)) {\n    continue;\n  }\n  if (!query.builders[filter].launder) {\n    continue;\n  }\n\n  // NEW: Enforce publicApiProjection restrictions on choices/counts\n  const publicApiProjection = query.get(\u0027project\u0027);\n  if (publicApiProjection \u0026\u0026 !publicApiProjection[filter]) {\n    continue;\n  }\n\n  // NEW: Enforce viewPermission field restrictions\n  const field = self.schema.find(f =\u003e f.name === filter);\n  if (field \u0026\u0026 field.viewPermission \u0026\u0026\n      !self.apos.permission.can(query.req, field.viewPermission.action, field.viewPermission.type)) {\n    continue;\n  }\n\n  const _query = baseQuery.clone();\n  _query[filter](null);\n  choices[filter] = await _query.toChoices(filter, { counts: query.get(\u0027counts\u0027) });\n}\n```\n\nAdditionally, apply the same fix in the page REST API handler (page/index.js) for consistency.",
  "id": "GHSA-c276-fj82-f2pq",
  "modified": "2026-04-16T20:45:15Z",
  "published": "2026-04-16T20:45:15Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-c276-fj82-f2pq"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39857"
    },
    {
      "type": "WEB",
      "url": "https://github.com/apostrophecms/apostrophe/commit/6c2b548dec2e3f7a82e8e16736603f4cd17525aa"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/apostrophecms/apostrophe"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "ApostropheCMS: Information Disclosure via choices/counts Query Parameters Bypassing publicApiProjection Field Restrictions"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…