GHSA-MQQ5-J7W8-2HGH

Vulnerability from github – Published: 2026-06-19 17:47 – Updated: 2026-06-19 17:47
VLAI
Summary
AlchemyCMS: Unauthenticated nested page API leaks restricted & unpublished content
Details

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

# 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

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

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 8.2.5"
      },
      "package": {
        "ecosystem": "RubyGems",
        "name": "alchemy_cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "8.2.0"
            },
            {
              "fixed": "8.2.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 8.1.13"
      },
      "package": {
        "ecosystem": "RubyGems",
        "name": "alchemy_cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "8.1.0"
            },
            {
              "fixed": "8.1.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 8.0.14"
      },
      "package": {
        "ecosystem": "RubyGems",
        "name": "alchemy_cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "8.0.0.a"
            },
            {
              "fixed": "8.0.15"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 7.4.14"
      },
      "package": {
        "ecosystem": "RubyGems",
        "name": "alchemy_cms"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "7.4.15"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "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 \u0026 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\u0027s 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 \u0027\"name\"|\"restricted\"|\"public\"\u0027\n\n# 2) Content leak of restricted page\ncurl -s \"http://localhost:3000/api/pages/nested?elements=true\" | grep -oE \u0027TOPSECRET_RESTRICTED_BODY_[A-Za-z0-9]+|RESTRICTED_RICHTEXT_[A-Za-z0-9]+\u0027\n\n# 3) Contrast \u2014 show denies the same guest\ncurl -s -o /dev/null -w \"show /api/pages/3 -\u003e 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`\u0027s `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-06-19T17:47:13Z",
  "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.4.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 \u0026 unpublished content"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…