ghsa-mqq5-j7w8-2hgh
CVSS 7.5 osv_rubygems# Unauthenticated nested page API leaks restricted & unpublished content - **Location:** `app/controllers/alchemy/api/pages_controller.rb:28` (`Api::PagesController#nested`) - **Affected version:** Alchemy CMS 8.3.0.dev (Rails 8.1.3) ## Description The unauthenticated `GET /api/pages/nested` endpoint returns the full page tree to any anonymous caller, including restricted (member-only) pages and unpublished/draft pages that should be hidden. Appending `?elements=true` additionally dumps the element/ingredient **content** of restricted pages, fully bypassing the access control the sibling `show` and `index` actions enforce. ## Root cause `Api::PagesController#nested` calls no `authorize!` and applies no `published`/`restricted` scoping, unlike `show` (`authorize! :show`) and `index` (`accessible_by(current_ability, :index)`). `PageTreePreloader` loads `page.self_and_descendants` unfiltered, and `PageTreeSerializer` emits every page's metadata (and, with `elements`, `public_version.elements`) with no ability check. ## Evidence An unauthenticated `GET /api/pages/nested` returns HTTP 200 with the restricted page (`"restricted":true`) and an unpublished draft (`"public":false`); `?elements=true` leaks its content (e.g. `TOPSECRET_RESTRICTED_BODY_proof123`). The same guest hitting `GET /api/pages/3` (`show`) gets HTTP **403** `{"error":"Not authorized"}`, proving `nested` returns what `show` correctly denies. ### Reproduction ```bash # 1) Metadata leak (guest, no auth) curl -s http://localhost:3000/api/pages/nested | python3 -m json.tool | grep -E '"name"|"restricted"|"public"' # 2) Content leak of restricted page curl -s "http://localhost:3000/api/pages/nested?elements=true" | grep -oE 'TOPSECRET_RESTRICTED_BODY_[A-Za-z0-9]+|RESTRICTED_RICHTEXT_[A-Za-z0-9]+' # 3) Contrast — show denies the same guest curl -s -o /dev/null -w "show /api/pages/3 -> HTTP %{http_code}\n" http://localhost:3000/api/pages/3 ``` ### Suggested fix ```ruby def nested @page = Page.find_by(id: params[:page_id]) || Language.current_root_page authorize! :show, @page preloaded_page = PageTreePreloader.new(page: @page, user: current_alchemy_user, ability: current_ability).call render json: PageTreeSerializer.new(preloaded_page, ability: current_ability, user: current_alchemy_user, elements: params[:elements]) end ``` Additionally scope `PageTreePreloader`'s `self_and_descendants` via `accessible_by(current_ability)` and gate element emission in `PageTreeSerializer#page_elements` behind `opts[:ability].can?(:show, page)`.
- Published
- unknown
- Last Modified
- unknown
CVSS details not available.
No product information available.
No linked vulnerabilities found.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "<= 8.2.5",
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-mqq5-j7w8-2hgh/GHSA-mqq5-j7w8-2hgh.json"
},
"package": {
"ecosystem": "RubyGems",
"name": "alchemy_cms",
"purl": "pkg:gem/alchemy_cms"
},
"ranges": [
{
"events": [
{
"introduced": "8.2.0"
},
{
"fixed": "8.2.6"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"8.2.0",
"8.2.1",
"8.2.2",
"8.2.3",
"8.2.4",
"8.2.5"
]
},
{
"database_specific": {
"last_known_affected_version_range": "<= 8.1.13",
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-mqq5-j7w8-2hgh/GHSA-mqq5-j7w8-2hgh.json"
},
"package": {
"ecosystem": "RubyGems",
"name": "alchemy_cms",
"purl": "pkg:gem/alchemy_cms"
},
"ranges": [
{
"events": [
{
"introduced": "8.1.0"
},
{
"fixed": "8.1.14"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"8.1.0",
"8.1.1",
"8.1.10",
"8.1.11",
"8.1.12",
"8.1.13",
"8.1.2",
"8.1.3",
"8.1.4",
"8.1.5",
"8.1.6",
"8.1.7",
"8.1.8",
"8.1.9"
]
},
{
"database_specific": {
"last_known_affected_version_range": "<= 8.0.14",
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-mqq5-j7w8-2hgh/GHSA-mqq5-j7w8-2hgh.json"
},
"package": {
"ecosystem": "RubyGems",
"name": "alchemy_cms",
"purl": "pkg:gem/alchemy_cms"
},
"ranges": [
{
"events": [
{
"introduced": "8.0.0.a"
},
{
"fixed": "8.0.15"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"8.0.0",
"8.0.0.a",
"8.0.0.b",
"8.0.0.c",
"8.0.1",
"8.0.10",
"8.0.11",
"8.0.12",
"8.0.13",
"8.0.14",
"8.0.2",
"8.0.3",
"8.0.4",
"8.0.5",
"8.0.6",
"8.0.7",
"8.0.8",
"8.0.9"
]
},
{
"database_specific": {
"last_known_affected_version_range": "<= 7.4.14",
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-mqq5-j7w8-2hgh/GHSA-mqq5-j7w8-2hgh.json"
},
"package": {
"ecosystem": "RubyGems",
"name": "alchemy_cms",
"purl": "pkg:gem/alchemy_cms"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.4.15"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"2.0",
"2.0.1",
"2.0.2",
"2.0.3",
"2.0.3.1",
"2.0.4",
"2.0.5",
"2.0.6.1",
"2.0.7",
"2.0.pre2",
"2.0.pre3",
"2.0.pre4",
"2.0.pre5",
"2.0.rc1",
"2.0.rc2",
"2.0.rc3",
"2.0.rc4",
"2.0.rc5",
"2.0.rc6",
"2.1",
"2.1.1",
"2.1.11",
"2.1.12",
"2.1.2",
"2.1.3",
"2.1.4",
"2.1.5",
"2.1.6",
"2.1.7",
"2.1.8",
"2.1.8.1",
"2.1.9",
"2.1.9.1",
"2.1.beta1",
"2.1.beta5",
"2.1.beta6",
"2.1.rc2",
"2.1.rc3",
"2.1.rc4",
"2.1.rc5",
"2.1.rc6",
"2.2.0",
"2.2.1",
"2.2.2",
"2.2.3.1",
"2.2.3.2",
"2.2.4",
"2.2.rc1",
"2.2.rc11",
"2.2.rc13",
"2.2.rc14",
"2.2.rc15",
"2.2.rc2",
"2.2.rc3",
"2.2.rc6",
"2.2.rc7",
"2.2.rc8",
"2.3.0",
"2.3.1",
"2.3.2",
"2.3.rc5",
"2.4.0",
"2.4.1",
"2.4.beta2",
"2.4.rc1",
"2.4.rc2",
"2.4.rc4",
"2.5.0",
"2.5.0.b2",
"2.5.0.b5",
"2.5.0.b9",
"2.5.0.rc3",
"2.5.1",
"2.5.2",
"2.5.2.1",
"2.5.2.2",
"2.5.3",
"2.5.3.1",
"2.6.0",
"2.6.0.rc5",
"2.6.1",
"2.6.2",
"2.6.2.1",
"2.6.3",
"2.7.0",
"2.7.1",
"2.7.2",
"2.7.3",
"2.7.4",
"2.7.5",
"2.8.1",
"2.8.2",
"2.8.3",
"2.9.0",
"2.9.1",
"3.0.0",
"3.0.0.rc5",
"3.0.0.rc6",
"3.0.0.rc7",
"3.0.0.rc8",
"3.0.1",
"3.0.2",
"3.0.3",
"3.0.4",
"3.1.0",
"3.1.0.beta1",
"3.1.0.beta2",
"3.1.0.beta3",
"3.1.0.beta4",
"3.1.0.beta5",
"3.1.0.beta6",
"3.1.0.rc1",
"3.1.0.rc2",
"3.1.0.rc3",
"3.1.1",
"3.1.3",
"3.2.0",
"3.2.0.beta",
"3.2.0.rc1",
"3.2.1",
"3.3.0",
"3.3.0.rc1",
"3.3.0.rc2",
"3.3.1",
"3.3.2",
"3.3.3",
"3.4.0",
"3.4.0.rc1",
"3.4.1",
"3.4.2",
"3.5.0",
"3.5.0.rc1",
"3.5.0.rc2",
"3.6.0",
"3.6.1",
"3.6.2",
"3.6.3",
"3.6.4",
"3.6.5",
"3.6.6",
"3.6.7",
"4.0.0",
"4.0.0.beta",
"4.0.0.rc1",
"4.0.0.rc2",
"4.0.1",
"4.0.2",
"4.0.3",
"4.0.4",
"4.0.5",
"4.0.6",
"4.1.0",
"4.1.0.beta",
"4.1.0.rc1",
"4.1.1",
"4.1.2",
"4.2.0",
"4.2.0.rc1",
"4.2.1",
"4.2.2",
"4.2.3",
"4.2.4",
"4.3.0",
"4.3.1",
"4.3.2",
"4.4.0",
"4.4.1",
"4.4.2",
"4.4.3",
"4.4.4",
"4.4.5",
"4.5.0",
"4.5.1",
"4.6.0",
"4.6.1",
"4.6.2",
"4.6.3",
"4.6.4",
"4.6.5",
"4.6.6",
"4.6.7",
"5.0.0",
"5.0.0.beta1",
"5.0.0.beta2",
"5.0.0.rc1",
"5.0.0.rc2",
"5.0.1",
"5.0.10",
"5.0.2",
"5.0.3",
"5.0.4",
"5.0.5",
"5.0.6",
"5.0.7",
"5.0.8",
"5.0.9",
"5.1.0",
"5.1.0.beta1",
"5.1.0.beta2",
"5.1.0.rc1",
"5.1.1",
"5.1.10",
"5.1.2",
"5.1.3",
"5.1.4",
"5.1.5",
"5.1.6",
"5.1.7",
"5.1.8",
"5.1.9",
"5.2.0",
"5.2.0.b1",
"5.2.0.rc1",
"5.2.1",
"5.2.2",
"5.2.3",
"5.2.4",
"5.2.5",
"5.2.6",
"5.2.7",
"5.3.0",
"5.3.1",
"5.3.2",
"5.3.3",
"5.3.4",
"5.3.5",
"5.3.6",
"5.3.7",
"5.3.8",
"6.0.0",
"6.0.0.b1",
"6.0.0.b2",
"6.0.0.b3",
"6.0.0.pre.b4",
"6.0.0.pre.b5",
"6.0.0.pre.b6",
"6.0.0.pre.rc1",
"6.0.0.pre.rc2",
"6.0.0.pre.rc3",
"6.0.0.pre.rc4",
"6.0.0.pre.rc5",
"6.0.0.pre.rc6",
"6.0.0.pre.rc7",
"6.0.1",
"6.0.10",
"6.0.11",
"6.0.12",
"6.0.13",
"6.0.14",
"6.0.2",
"6.0.3",
"6.0.4",
"6.0.5",
"6.0.6",
"6.0.7",
"6.0.8",
"6.0.9",
"6.1.0",
"6.1.1",
"6.1.10",
"6.1.2",
"6.1.3",
"6.1.4",
"6.1.5",
"6.1.6",
"6.1.7",
"6.1.8",
"6.1.9",
"7.0.0",
"7.0.0.pre.a",
"7.0.0.pre.b",
"7.0.0.pre.c",
"7.0.0.pre.rc1",
"7.0.1",
"7.0.10",
"7.0.11",
"7.0.12",
"7.0.13",
"7.0.14",
"7.0.15",
"7.0.16",
"7.0.2",
"7.0.3",
"7.0.4",
"7.0.5",
"7.0.6",
"7.0.7",
"7.0.8",
"7.0.9",
"7.1.0",
"7.1.0.pre.b1",
"7.1.0.pre.b2",
"7.1.0.pre.rc1",
"7.1.1",
"7.1.10",
"7.1.11",
"7.1.12",
"7.1.13",
"7.1.2",
"7.1.3",
"7.1.4",
"7.1.5",
"7.1.6",
"7.1.7",
"7.1.8",
"7.1.9",
"7.2.0",
"7.2.0.b",
"7.2.0.rc1",
"7.2.0.rc2",
"7.2.1",
"7.2.2",
"7.2.3",
"7.2.4",
"7.2.5",
"7.2.6",
"7.2.7",
"7.2.8",
"7.2.9",
"7.3.0",
"7.3.1",
"7.3.2",
"7.3.3",
"7.3.4",
"7.3.5",
"7.3.6",
"7.3.7",
"7.3.8",
"7.4.0",
"7.4.1",
"7.4.10",
"7.4.11",
"7.4.12",
"7.4.13",
"7.4.14",
"7.4.2",
"7.4.3",
"7.4.4",
"7.4.5",
"7.4.6",
"7.4.7",
"7.4.8",
"7.4.9"
]
}
],
"aliases": [
"CVE-2026-57579"
],
"database_specific": {
"cwe_ids": [
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-19T17:47:13Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "# Unauthenticated nested page API leaks restricted & unpublished content\n\n- **Location:** `app/controllers/alchemy/api/pages_controller.rb:28` (`Api::PagesController#nested`)\n- **Affected version:** Alchemy CMS 8.3.0.dev (Rails 8.1.3)\n\n## Description\n\nThe unauthenticated `GET /api/pages/nested` endpoint returns the full page tree to any anonymous caller, including restricted (member-only) pages and unpublished/draft pages that should be hidden.\nAppending `?elements=true` additionally dumps the element/ingredient **content** of restricted pages, fully bypassing the access control the sibling `show` and `index` actions enforce.\n\n## Root cause\n\n`Api::PagesController#nested` calls no `authorize!` and applies no `published`/`restricted` scoping, unlike `show` (`authorize! :show`) and `index` (`accessible_by(current_ability, :index)`).\n`PageTreePreloader` loads `page.self_and_descendants` unfiltered, and `PageTreeSerializer` emits every page's metadata (and, with `elements`, `public_version.elements`) with no ability check.\n\n## Evidence\n\nAn unauthenticated `GET /api/pages/nested` returns HTTP 200 with the restricted page (`\"restricted\":true`) and an unpublished draft (`\"public\":false`); `?elements=true` leaks its content (e.g. `TOPSECRET_RESTRICTED_BODY_proof123`).\nThe same guest hitting `GET /api/pages/3` (`show`) gets HTTP **403** `{\"error\":\"Not authorized\"}`, proving `nested` returns what `show` correctly denies.\n\n### Reproduction\n\n```bash\n# 1) Metadata leak (guest, no auth)\ncurl -s http://localhost:3000/api/pages/nested | python3 -m json.tool | grep -E '\"name\"|\"restricted\"|\"public\"'\n\n# 2) Content leak of restricted page\ncurl -s \"http://localhost:3000/api/pages/nested?elements=true\" | grep -oE 'TOPSECRET_RESTRICTED_BODY_[A-Za-z0-9]+|RESTRICTED_RICHTEXT_[A-Za-z0-9]+'\n\n# 3) Contrast — show denies the same guest\ncurl -s -o /dev/null -w \"show /api/pages/3 -> HTTP %{http_code}\\n\" http://localhost:3000/api/pages/3\n```\n\n### Suggested fix\n\n```ruby\ndef nested\n @page = Page.find_by(id: params[:page_id]) || Language.current_root_page\n authorize! :show, @page\n preloaded_page = PageTreePreloader.new(page: @page, user: current_alchemy_user, ability: current_ability).call\n render json: PageTreeSerializer.new(preloaded_page, ability: current_ability,\n user: current_alchemy_user, elements: params[:elements])\nend\n```\n\nAdditionally scope `PageTreePreloader`'s `self_and_descendants` via `accessible_by(current_ability)` and gate element emission in `PageTreeSerializer#page_elements` behind `opts[:ability].can?(:show, page)`.",
"id": "GHSA-mqq5-j7w8-2hgh",
"modified": "2026-09-15T03:56:01.761610104Z",
"published": "2026-06-19T17:47:13Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/AlchemyCMS/alchemy_cms/security/advisories/GHSA-mqq5-j7w8-2hgh"
},
{
"type": "PACKAGE",
"url": "https://github.com/AlchemyCMS/alchemy_cms"
}
],
"schema_version": "1.9.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "AlchemyCMS: Unauthenticated nested page API leaks restricted & unpublished content"
}