Skip to content

Commit cd8085d

Browse files
1 parent 6ada108 commit cd8085d

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-c276-fj82-f2pq",
4+
"modified": "2026-04-16T20:45:15Z",
5+
"published": "2026-04-16T20:45:15Z",
6+
"aliases": [
7+
"CVE-2026-39857"
8+
],
9+
"summary": "ApostropheCMS: Information Disclosure via choices/counts Query Parameters Bypassing publicApiProjection Field Restrictions",
10+
"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's `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'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.\n\n5. 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`.\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 'article' piece type with:\n# publicApiProjection: { title: 1, slug: 1, _url: 1 }\n# and additional schema fields like 'status' (select), 'priority' (integer),\n# or 'internalNotes' (string) NOT in the projection\n\n# 1. Verify normal API access only returns projected fields\ncurl -s 'http://localhost:3000/api/v1/article' | 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 'http://localhost:3000/api/v1/article?choices=status' | 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 'http://localhost:3000/api/v1/article?counts=priority' | 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 'http://localhost:3000/api/v1/article?choices=status,priority,internalNotes'\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's `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's 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('project');\n if (publicApiProjection && !publicApiProjection[filter]) {\n continue;\n }\n\n // NEW: Enforce viewPermission field restrictions\n const field = self.schema.find(f => f.name === filter);\n if (field && field.viewPermission &&\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('counts') });\n}\n```\n\nAdditionally, apply the same fix in the page REST API handler (page/index.js) for consistency.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "apostrophe"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "4.29.0"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 4.28.0"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-c276-fj82-f2pq"
45+
},
46+
{
47+
"type": "ADVISORY",
48+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39857"
49+
},
50+
{
51+
"type": "WEB",
52+
"url": "https://github.com/apostrophecms/apostrophe/commit/6c2b548dec2e3f7a82e8e16736603f4cd17525aa"
53+
},
54+
{
55+
"type": "PACKAGE",
56+
"url": "https://github.com/apostrophecms/apostrophe"
57+
}
58+
],
59+
"database_specific": {
60+
"cwe_ids": [
61+
"CWE-200"
62+
],
63+
"severity": "MODERATE",
64+
"github_reviewed": true,
65+
"github_reviewed_at": "2026-04-16T20:45:15Z",
66+
"nvd_published_at": "2026-04-15T20:16:36Z"
67+
}
68+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-f8hv-g549-hwg2",
4+
"modified": "2026-04-16T20:45:04Z",
5+
"published": "2026-04-16T20:45:04Z",
6+
"aliases": [
7+
"CVE-2026-39845"
8+
],
9+
"summary": "Weblate: SSRF via the webhook add-on using unprotected fetch_url()",
10+
"details": "### Impact\nThe webhook add-on did not utilize existing SSRF protection.\n\n### Patches\n* https://github.com/WeblateOrg/weblate/pull/18815\n\n### Workarounds\nDisabling the add-on would avoid misusing this.\n\n### References\nThanks to @Lihfdgjr for reporting this via GitHub.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "weblate"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "5.17"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/WeblateOrg/weblate/security/advisories/GHSA-f8hv-g549-hwg2"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39845"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/WeblateOrg/weblate/pull/18815"
50+
},
51+
{
52+
"type": "PACKAGE",
53+
"url": "https://github.com/WeblateOrg/weblate"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-918"
59+
],
60+
"severity": "MODERATE",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-04-16T20:45:04Z",
63+
"nvd_published_at": "2026-04-15T19:16:36Z"
64+
}
65+
}

0 commit comments

Comments
 (0)