PT-2026-53790 · Rubygems · Alchemycms

Publicado

2026-06-19

·

Atualizado

2026-06-19

CVSS v3.1

7.5

Alta

VetorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

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

Correção

Missing Authorization

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

GHSA-MQQ5-J7W8-2HGH

Produtos afetados

Alchemycms