Mass-assignment in ingredients_attributes enables STI type confusion and polymorphic-association tampering, leading to stored XSS and Author -> Admin privilege escalation
Published: November 09, 2026
SECURITY IDENTIFIERS
- GHSA: GHSA-3v87-v27q-vf34
- Vendor Advisory: https://github.com/AlchemyCMS/alchemy_cms/security/advisories/GHSA-3v87-v27q-vf34
GEM
SEVERITY
CVSS v3.x: 5.4 (Medium)
UNAFFECTED VERSIONS
< 6.0.0
PATCHED VERSIONS
>= 8.4.3
DESCRIPTION
Alchemy::Admin::ElementsController' permitted nested ingredient attributes with an open hash (ingredients_attributes: {}), and Alchemy::Element accepted them for every ingredient column. A user allowed to edit elements, which in the default role set includes author, the lowest content role, could send a modified PATCH /admin/elements/:id request and:
- change an ingredient's STI type, for example turn a Text ingredient into an Html ingredient, whose view renders its value unescaped;
- point an ingredient's polymorphic related_object at a record of any model in the application;
- create ingredients of any type and role that the element's definition does not declare;
- replace an ingredient's whole data store.
No crafted code is needed. The request is an ordinary authenticated form submission, and type and related_object_type only have to name classes that already exist, such as Alchemy::Ingredients::Html.
Impact
Changing the type gives an author a way to store markup that is rendered unescaped, both on the public site and in the admin page preview where editors and administrators review drafts. A script stored this way runs with the privileges of whoever views the page, so an administrator reviewing an author's draft can have actions performed in their session.
This adds the most risk on sites that deliberately keep authors from storing HTML, by using only text-like ingredients or by configuring a sanitizer for Richtext ingredients. Richtext ingredients without a sanitizer setting render their HTML as entered, so on sites using them authors can already store markup.
Pointing related_object at an unrelated record mostly breaks rendering of that ingredient. It exposes data only where the ingredient's view renders something of the related record, for example its to_s.
