Skip to content

Commit 4292ffa

Browse files
committed
Cover the re-inflation entry point with an editor.loadHTML security test
Add a system test group that drives the real production entry point (editor.loadHTML) rather than the sanitizer in isolation: it re-parses stored HTML under SAFE_FOR_XML and renders it into the live editor, including attachment content re-parsed by AttachmentView. It asserts the security invariant — no live event-handler attribute, no <script>, and no execution — which is a browser-independent DOMPurify guarantee and so holds across the Sauce matrix.
1 parent d60eb5f commit 4292ffa

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

src/test/system.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ import "test/system/list_formatting_test"
1818
import "test/system/morphing_test"
1919
import "test/system/mutation_input_test"
2020
import "test/system/pasting_test"
21+
import "test/system/reinflation_security_test"
2122
import "test/system/text_formatting_test"
2223
import "test/system/undo_test"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { assert, test, testGroup } from "test/test_helper"
2+
3+
import { delay } from "../test_helpers/timing_helpers"
4+
5+
// getEditorElement is installed as a global test helper (see trix/core/helpers/global),
6+
// mirroring how the other system tests reach the live editor.
7+
8+
// Exercises the real re-inflation entry point rather than the sanitizer in isolation:
9+
// editor.loadHTML re-parses stored HTML under DOMPurify's mXSS-safe mode
10+
// (SAFE_FOR_XML: true) and renders it into the live editor, including any attachment
11+
// content re-parsed by AttachmentView. These assertions target the security invariant —
12+
// no executable handler and no <script> reach the live DOM, and nothing executes.
13+
// Handler stripping is a browser-independent DOMPurify guarantee, so unlike element
14+
// shape the assertions are parser-agnostic.
15+
testGroup("Re-inflation security (editor.loadHTML)", { template: "editor_empty" }, () => {
16+
const loadAndAssertInert = async (html) => {
17+
window.reinflationXSS = 0
18+
getEditorElement().editor.loadHTML(html)
19+
await delay(20)
20+
21+
const element = getEditorElement()
22+
assert.equal(
23+
element.querySelectorAll("[onerror], [onload], [onclick]").length, 0,
24+
`live event handler survived re-inflation: ${element.innerHTML}`
25+
)
26+
assert.notOk(element.querySelector("script"), `script survived re-inflation: ${element.innerHTML}`)
27+
assert.equal(window.reinflationXSS, 0, "re-inflated payload executed")
28+
29+
delete window.reinflationXSS
30+
}
31+
32+
test("neutralizes a mutation-XSS payload loaded through the editor", async () => {
33+
await loadAndAssertInert(
34+
"<noscript><p title=\"</noscript><img src=x onerror=window.reinflationXSS=(window.reinflationXSS||0)+1>\">"
35+
)
36+
})
37+
38+
test("sanitizes attacker-controlled attachment content on re-inflation", async () => {
39+
const attachment = {
40+
contentType: "text/html5",
41+
content: "</style><img src=x onerror=window.reinflationXSS=(window.reinflationXSS||0)+1>HELLO",
42+
}
43+
const html = `<div data-trix-attachment='${JSON.stringify(attachment).replace(/'/g, "&#39;")}'></div>`
44+
await loadAndAssertInert(html)
45+
})
46+
})

0 commit comments

Comments
 (0)