CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



GHSA-QMPG-8XG6-PH5Q
Unassigned (Moderate)

GHSA-QMPG-8XG6-PH5Q: Stored Cross-Site Scripting via Sanitizer Bypass in Trix Editor

Alon Barad
Alon Barad
Software Engineer

Mar 13, 2026·8 min read·2 visits

PoC Available

Executive Summary (TL;DR)

Trix versions prior to 2.1.17 are vulnerable to Stored XSS. A custom DOMPurify hook permits the `data-trix-serialized-attributes` attribute to bypass sanitization. Trix later parses this attribute and applies its contents to the DOM, allowing attackers to inject malicious event handlers.

A stored Cross-Site Scripting (XSS) vulnerability exists in the Trix rich text editor, distributed via the `trix` npm package and the `action_text-trix` RubyGem. The flaw occurs due to a bypass in the DOMPurify sanitization configuration, where a custom hook improperly whitelists the `data-trix-serialized-attributes` attribute. This allows an attacker to inject serialized JSON payloads containing malicious JavaScript handlers, which Trix subsequently deserializes and applies directly to the live DOM, leading to arbitrary code execution within the context of the victim's browser.

Vulnerability Overview

The Trix rich text editor is a widely used component for handling user-formatted content, functioning as the default editor for the ActionText module in Ruby on Rails applications. Rich text editors inherently expand the attack surface of an application by accepting, processing, and rendering complex HTML structures. To mitigate the risks associated with raw HTML input, Trix integrates DOMPurify, a robust client-side HTML sanitization library, to strip dangerous tags and attributes before they are integrated into the Document Object Model (DOM).

The core of this vulnerability lies in the tension between strict security sanitization and the application-specific functionality required by Trix. Trix relies on custom HTML data attributes to maintain internal state, track attachments, and manage formatting context. To prevent DOMPurify from stripping these operational attributes, Trix registers a custom hook that explicitly whitelists any attribute beginning with the data-trix- prefix. This broad inclusion rule creates a structural weakness in the sanitization pipeline.

The vulnerability is classified as Improper Neutralization of Input During Web Page Generation (CWE-79). Specifically, it is a Stored Cross-Site Scripting (XSS) flaw that abuses an internal state restoration mechanism. When Trix processes an element containing the data-trix-serialized-attributes attribute, it extracts the JSON-encoded string within, parses it, and dynamically applies the resulting key-value pairs back to the HTML element. By smuggling a malicious payload through the DOMPurify whitelist, an attacker can leverage this deserialization phase to inject dangerous attributes that execute JavaScript when the payload is rendered by a victim.

Root Cause Analysis

The root cause of this vulnerability is a design flaw in how the Trix DOMPurify configuration defines its whitelist boundary. DOMPurify provides an event hook mechanism that allows developers to intervene during the sanitization process. Trix utilizes the uponSanitizeAttribute hook to inspect each attribute before DOMPurify makes a final keep-or-discard decision. The implementation evaluates the attribute name against a regular expression (/^data-trix-/) and sets data.forceKeepAttr = true if a match occurs.

Setting data.forceKeepAttr = true instructs DOMPurify to unconditionally retain the attribute, bypassing any subsequent internal security checks that would normally scrutinize the attribute's content for malicious payloads. This assumes that all data-trix-* attributes are inherently safe and strictly controlled by the Trix editor's internal logic. However, this assumption fails when processing external input, as an attacker can manually construct these attributes to mimic internal state data.

The specific attribute involved in the exploit is data-trix-serialized-attributes. This attribute acts as a storage vector for a JSON-serialized map of HTML attributes. The critical failure occurs during the post-sanitization phase, where Trix initiates an internal state restoration process. The editor reads the value of the data-trix-serialized-attributes attribute, executes JSON.parse() on the string, and iterates over the resulting object. For each key-value pair, Trix calls the native element.setAttribute() method to apply the attribute directly to the live DOM node.

Because the sanitization phase only evaluates the attribute's name and completely ignores its JSON-encoded payload, the system provides a clear pathway for secondary injection. The attacker does not need to bypass the HTML parser; they only need to bypass the attribute name filter. Once the JSON payload safely traverses the sanitizer, the Trix editor acts as a confused deputy, programmatically extracting the malicious payload from the JSON object and binding it to the DOM where the browser will execute it.

Code Analysis and Patch Details

The vulnerability resides in the src/trix/models/html_sanitizer.js file, where the DOMPurify hooks are initialized. The vulnerable implementation applies a broad regular expression to whitelist attributes without verifying the specific attribute or its contents. The patch introduced in commit 53197ab5a142e6b0b76127cb790726b274eaf1bc resolves this by introducing a highly specific blocklist entry immediately before the broad whitelist logic is executed.

// Vulnerable Implementation (Prior to 2.1.17)
DOMPurify.addHook("uponSanitizeAttribute", function (node, data) {
  const allowedAttributePattern = /^data-trix-/
  if (allowedAttributePattern.test(data.attrName)) {
    data.forceKeepAttr = true
  }
})

The vulnerable code above demonstrates the blind trust placed in the data-trix- prefix. Any attribute meeting this condition is forced through the sanitizer. The fix modifies this hook to explicitly intercept and discard the data-trix-serialized-attributes attribute, regardless of the subsequent regex evaluation.

// Patched Implementation (2.1.17)
DOMPurify.addHook("uponSanitizeAttribute", function (node, data) {
  if (data.attrName === "data-trix-serialized-attributes") {
    data.keepAttr = false
    return
  }
 
  const allowedAttributePattern = /^data-trix-/
  if (allowedAttributePattern.test(data.attrName)) {
    data.forceKeepAttr = true
  }
})

By setting data.keepAttr = false and immediately returning from the function, the patched code ensures that DOMPurify permanently discards the data-trix-serialized-attributes attribute during the sanitization pass. This severs the attack vector by preventing the malicious JSON payload from ever reaching the Trix deserialization routines.

While this fix directly addresses the known exploitation path, it relies on a specific blocklist entry rather than a fundamental redesign of the attribute parsing logic. Security engineers should recognize that broad prefix-based whitelisting (forceKeepAttr = true) remains a high-risk architectural pattern in sanitization pipelines. If future updates introduce new data-trix-* attributes that dynamically modify the DOM, similar vulnerabilities could emerge.

Exploitation Methodology

Exploiting this vulnerability requires the attacker to submit a carefully crafted HTML payload to an application endpoint that stores and renders Trix-formatted content. The primary prerequisite is the ability to submit rich text data, which is typically available to any authenticated user in systems featuring comment sections, forums, or profile descriptions. No special configuration or elevated privileges are required to trigger the flaw, as the vulnerability resides entirely within the standard input processing pipeline.

The attack methodology leverages a nested payload structure designed to survive the initial DOMPurify pass and trigger the internal Trix parsing logic. The proof-of-concept payload utilizes a data-trix-attachment structure to encapsulate the secondary injection vector. This encapsulation ensures that the payload is treated as a complex attachment object rather than raw HTML, prompting Trix to execute its state restoration routines.

<div data-trix-attachment="{
    &quot;contentType&quot;:&quot;text/html&quot;,
    &quot;content&quot;:&quot;&lt;img src=\&quot;x\&quot; data-trix-serialized-attributes=\&quot;{&amp;quot;onerror&amp;quot;:&amp;quot;alert(1)&amp;quot;}\&quot;&gt;&quot;
}"></div>

The payload operates in several distinct phases. First, the application receives the HTML input and passes it to Trix. Trix processes the raw HTML through DOMPurify. When DOMPurify encounters the img tag within the content string, it triggers the uponSanitizeAttribute hook. The data-trix-serialized-attributes attribute begins with the trusted prefix, so DOMPurify sets forceKeepAttr = true and allows the attribute to remain intact in the sanitized output.

In the final phase, Trix takes the sanitized output and begins processing the custom attachment. It extracts the img element and identifies the data-trix-serialized-attributes attribute. Trix reads the value {"onerror":"alert(1)"}, parses it as JSON, and invokes element.setAttribute('onerror', 'alert(1)') on the live DOM node. Because the img tag specifies an invalid source (src="x"), the browser immediately triggers the newly injected onerror event handler, resulting in arbitrary JavaScript execution.

Impact Assessment

The impact of a Stored Cross-Site Scripting vulnerability in a widely adopted rich text editor is substantial. When an attacker successfully injects a malicious payload, the JavaScript executes within the context of the victim's session whenever the stored content is rendered. This execution context provides the attacker with full access to the Document Object Model (DOM), the window object, and any session material not protected by the HttpOnly flag.

Because Trix is frequently deployed via ActionText in Ruby on Rails applications, the injected content is often rendered across multiple administrative boundaries. For example, a low-privileged user might submit a malicious payload in a support ticket or a forum post. When a system administrator views that content in a backend dashboard, the payload executes with the administrator's privileges. This effectively converts a low-privileged input vector into an administrative account compromise.

An attacker leveraging this execution capability can silently perform arbitrary actions on behalf of the victim. This includes issuing unauthorized API requests, modifying application data, or exfiltrating sensitive information displayed on the page. Furthermore, the attacker can use the compromised session to interact with application interfaces, circumventing standard Cross-Site Request Forgery (CSRF) protections by reading valid tokens directly from the DOM.

Remediation and Mitigation

The primary and most effective remediation for this vulnerability is upgrading the affected dependencies to the patched version. Development teams must ensure that their package manifests specify version 2.1.17 or higher for both the trix npm package and the action_text-trix RubyGem. After updating the manifests, developers should execute npm update trix or bundle update action_text-trix to fetch and install the secure release.

In scenarios where an immediate dependency upgrade is not feasible, security teams can implement server-side mitigation strategies to neutralize the attack vector. Because the vulnerability relies entirely on the presence of the data-trix-serialized-attributes attribute, backend sanitization pipelines can be configured to forcefully strip this specific attribute from all incoming HTML payloads before the data is persisted to the database. This breaks the exploitation chain by removing the serialization sink entirely.

As a broader defense-in-depth measure, applications should implement a strict Content Security Policy (CSP). A robust CSP that restricts script execution to trusted sources and explicitly forbids inline scripts (by omitting 'unsafe-inline') will prevent the injected onerror handler from executing, even if the payload successfully bypasses the Trix sanitization logic. While CSP does not fix the underlying parsing flaw, it provides a critical safety net against exploitation.

Official Patches

BasecampPull Request #1282 containing the sanitization bypass fix.
BasecampRelease Notes for Trix v2.1.17.

Fix Analysis (1)

Technical Appendix

CVSS Score
Unassigned (Moderate)/ 10

Affected Systems

action_text-trix (RubyGems)trix (npm)

Affected Versions Detail

Product
Affected Versions
Fixed Version
action_text-trix
Basecamp / Ruby on Rails
< 2.1.172.1.17
trix
Basecamp
< 2.1.172.1.17
AttributeDetail
Vulnerability TypeStored Cross-Site Scripting (XSS)
CWE IDCWE-79
Attack VectorNetwork / Web Input
Authentication RequiredNone (Context Dependent)
CVSS SeverityModerate
Exploit StatusProof of Concept Available
CISA KEVNot Listed

MITRE ATT&CK Mapping

T1190Exploit Public-Facing Application
Initial Access
T1059.007Command and Scripting Interpreter: JavaScript
Execution
CWE-79
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.

Vulnerability Timeline

Fix commit and Pull Request #1282 merged into main branch.
2026-03-11
Trix version 2.1.17 published.
2026-03-11
Vulnerability published to the GitHub Advisory Database as GHSA-QMPG-8XG6-PH5Q.
2026-03-13

References & Sources

  • [1]GitHub Advisory: GHSA-QMPG-8XG6-PH5Q
  • [2]Trix Pull Request #1282
  • [3]Trix Fix Commit
  • [4]Trix v2.1.17 Release Notes

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.