GHSA-MQQ5-J7W8-2HGH: Authorization Added to Alchemy CMS Nested Pages API
Summary
The patch addresses a missing authorization check in Alchemy CMS's API nested pages endpoint by enforcing page-level authorization at the controller entry point and propagating ability-based filtering into tree preloading and element serialization. The change materially fixes the data exposure path described in the advisory: unauthenticated or low-privilege callers could previously enumerate nested pages and, with elements enabled, extract content from restricted descendants. The remediation is defense-in-depth rather than a single guard, and the added tests demonstrate both denial for unauthorized roots and pruning of unreadable descendants and elements.
Analysis
Vulnerability
GHSA-MQQ5-J7W8-2HGH describes a missing authorization flaw in the Alchemy CMS API pages controller. The vulnerable path was the nested pages endpoint, where the controller preloaded a full page subtree from @page without first authorizing access to that root page and without constraining descendant traversal to the caller's readable scope. As shown in the patch summary, the old implementation invoked PageTreePreloader.new(page: @page, user: current_alchemy_user).call, and the preloader itself used page.self_and_descendants directly. That combination allowed unauthenticated or underprivileged callers to dump page structure beyond their authorization boundary.
The impact was not limited to metadata enumeration. The patch also hardens the serializer to suppress elements when the provided ability cannot read the underlying page, which indicates that element content from restricted pages could otherwise be exposed when elements=true was requested. The new controller and serializer tests explicitly assert that guest users must not see restricted or unpublished pages, must not receive restricted page content, and must receive 403 when directly requesting a restricted page as the root.
# Vulnerable flow from patch summary
# controller
preloaded_page = PageTreePreloader.new(page: @page, user: current_alchemy_user).call
# service
pages = page.self_and_descendantsSources: official patch commit, GitHub Security Advisory, third-party report.
Patch
The patch introduces authorization enforcement at three layers.
Controller gate: the API controller now performs
authorize! :show, @pagebefore building the nested response. This closes the direct access case where a caller could specify a restrictedpage_idand receive a tree rooted at an unreadable page.Ability-scoped tree loading:
PageTreePreloadernow requires anability:argument and replaces unrestricted descendant loading withpage.self_and_descendants.accessible_by(ability, :read). This is the core fix for subtree overexposure because it prunes unreadable descendants before serialization.Serializer hardening:
PageTreeSerializernow unwraps delegator-backed page objects and checksopts[:ability].can?(:read, authorized_page)before returning elements. If unreadable, it returnsAlchemy::Element.none. This prevents content leakage even if a restricted page object reaches the serializer.
authorize! :show, @page
preloaded_page = PageTreePreloader.new(
page: @page,
user: current_alchemy_user,
ability: current_ability
).call
# service
pages = page.self_and_descendants.accessible_by(ability, :read)
# serializer
authorized_page = page.try(:__getobj__) || page
return Alchemy::Element.none unless opts[:ability].can?(:read, authorized_page)The tests added in the commit are aligned with the vulnerability mechanics. Controller specs verify that guest users do not see restricted or unpublished descendants, do not receive restricted element content, and are denied direct access to a restricted root page. Service specs verify that the preloader includes only readable children for a guest ability. Serializer specs verify that element emission is suppressed for unreadable pages even when a plain Alchemy::Page is passed instead of the usual delegator wrapper. Source: official patch commit.
Review
Pros
- The fix addresses the actual authorization boundary failure, not just the symptom. The root page is now explicitly authorized, and descendants are filtered through
accessible_by(ability, :read). - The remediation is defense-in-depth. Even if an unreadable page object reaches serialization, element content is still suppressed by an ability check.
- The preloader API was tightened to require
ability:, reducing the chance of future call sites accidentally loading unrestricted trees. - Test coverage is strong and directly tied to exploitability: unauthorized root access, descendant pruning, unpublished/restricted visibility, and element leakage are all exercised.
- The serializer's delegator unwrapping is a practical hardening detail. It avoids authorization checks being evaluated against the wrapper class instead of
Alchemy::Page.
Cons
- The controller authorizes with
:showwhile the preloader and serializer scope with:read. If the application's ability rules distinguish these actions, the endpoint could still exhibit inconsistent behavior between root authorization and descendant filtering. The patch likely relies on Alchemy's action aliases, but that assumption is not demonstrated in the provided sources. - The fix is localized to this endpoint and related helpers. It does not by itself prove that other API endpoints consuming page trees or elements consistently pass
ability:and enforce equivalent checks. - The serializer safeguard only protects element emission. If future fields beyond elements are added and are sensitive, they will need equivalent authorization-aware handling.
Verdict
Root-cause.
This patch fixes the underlying authorization flaw in the nested pages API by enforcing access control at the root object, constraining descendant expansion to readable pages, and preventing restricted element serialization. The changes are source-consistent with the advisory and the tests demonstrate that the previously exposed data paths are now blocked for guest users while remaining available to authorized users. The only notable review point is the mixed use of :show and :read; assuming Alchemy's ability model aliases them appropriately, the patch is technically sound and substantially complete.
Recommended Labs
Try this vulnerability pattern yourself with hands-on labs.
- Codehub.rb
Best match for this GHSA because the advisory describes missing authorization on a Ruby/Rails CMS API endpoint, which maps directly to broken access control and object-level authorization failures (CWE-285/CWE-639 style issues). This Ruby hands-on lab is tagged to OWASP A01:2021 and focuses on defensive fixes around authorization boundaries.
- BadAuthz GraphQL.ts
Strong conceptual fit for an API endpoint missing an authorization check. Even though it uses GraphQL/TypeScript instead of Ruby, it is explicitly centered on bad authorization and object access control, making it useful practice for reviewing API handlers that expose nested resources without verifying caller permissions.
- No Sutpo.py
Useful supplemental lab for practicing insecure direct object access and missing authorization patterns. The Alchemy CMS issue allows unauthenticated access to sensitive nested page data; this lab reinforces defensive thinking around resource ownership checks, route protection, and consistent authorization enforcement across endpoints.