GHSA-mqq5-j7w8-2hghHigh· 7.5▾ TwilightAlchemyCMS: Unauthenticated nested page API leaks restricted & unpublished content
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 41.3 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
app/controllers/alchemy/api/pages_controller.rb:28 (Api::PagesController#nested)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.
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.
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.
# 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
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).
alchemy_cms >= 8.2.0, <= 8.2.5alchemy_cms >= 8.1.0, <= 8.1.13alchemy_cms >= 8.0.0.a, <= 8.0.14alchemy_cms <= 7.4.14Upgrade to a patched release:
alchemy_cms 8.2.6alchemy_cms 8.1.14alchemy_cms 8.0.15alchemy_cms 7.4.15Connected by shared product, vendor, weakness, or advisory.
CVE-2026-57579High· 7.5Alchemy is an open source content management system engine written in Ruby on Rails
CVE-2026-86777Medium· 5.3AlchemyCMS versions before 7.4.16 and 8.x before 8.3.6 fail to authorize access to the GET /api/nodes endpoint, allowing unauthenticated attackers to retrieve all navigation nodes
CVE-2024-0829Medium· 4.3The Comments Extra Fields For Post,Pages and CPT plugin for WordPress is vulnerable to Missing Authorization in all versions up to, and including, 5.0
CVE-2026-11807Critical· 9.6A missing authorization vulnerability was found in the Event-Driven Ansible (EDA) websocket API
CVE-2025-13772High· 7.1GitLab has remediated an issue in GitLab EE affecting all versions from 18.4 before 18.5.5, 18.6 before 18.6.3, and 18.7 before 18.7.1 that could have allowed an authenticated user to access and utilize AI model settings from unauthorize…
CVE-2026-12515Medium· 4.3katello: missing repository authorization in content_uploads exposes cross-product content existence