Skip to content

Commit 3282411

Browse files
jorgemanrubiajeremy
authored andcommitted
Keep data-trix attributes when sanitizing for XML
DOMPurify removes an attribute whose value contains `</style>`, `</title>`, `</textarea>`, `-->` or `]>` before it honors `forceKeepAttr`, so the hook that protects `data-trix-*` never takes effect under `SAFE_FOR_XML`. Stash those values and restore them in `afterSanitizeAttributes` instead.
1 parent c27f3ee commit 3282411

5 files changed

Lines changed: 76 additions & 6 deletions

File tree

action_text-trix/app/assets/javascripts/trix.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4248,16 +4248,30 @@ $\
42484248
}
42494249
var purify = createDOMPurify();
42504250

4251+
const ALLOWED_ATTRIBUTE_PATTERN = /^data-trix-/;
4252+
4253+
// DOMPurify's SAFE_FOR_XML check drops attributes whose values contain markup before it
4254+
// honors forceKeepAttr, so allowed attributes are stashed here and restored afterwards.
4255+
let stashedAttributes = [];
42514256
purify.addHook("uponSanitizeAttribute", function (node, data) {
42524257
if (data.attrName === "data-trix-serialized-attributes") {
42534258
data.keepAttr = false;
42544259
return;
42554260
}
4256-
const allowedAttributePattern = /^data-trix-/;
4257-
if (allowedAttributePattern.test(data.attrName)) {
4261+
if (ALLOWED_ATTRIBUTE_PATTERN.test(data.attrName)) {
42584262
data.forceKeepAttr = true;
4263+
stashedAttributes.push([data.attrName, node.getAttribute(data.attrName)]);
42594264
}
42604265
});
4266+
purify.addHook("afterSanitizeAttributes", function (node) {
4267+
stashedAttributes.forEach(_ref => {
4268+
let [name, value] = _ref;
4269+
if (value !== null && !node.hasAttribute(name)) {
4270+
node.setAttribute(name, value);
4271+
}
4272+
});
4273+
stashedAttributes = [];
4274+
});
42614275
const DEFAULT_ALLOWED_ATTRIBUTES = "style href src width height language class".split(" ");
42624276
const DEFAULT_FORBIDDEN_PROTOCOLS = "javascript:".split(" ");
42634277
const DEFAULT_FORBIDDEN_ELEMENTS = "script iframe form noscript".split(" ");
@@ -4330,10 +4344,10 @@ $\
43304344
element.removeAttribute("href");
43314345
}
43324346
}
4333-
Array.from(element.attributes).forEach(_ref => {
4347+
Array.from(element.attributes).forEach(_ref2 => {
43344348
let {
43354349
name
4336-
} = _ref;
4350+
} = _ref2;
43374351
if (!this.allowedAttributes.includes(name) && name.indexOf("data-trix") !== 0) {
43384352
element.removeAttribute(name);
43394353
}

src/test/system/pasting_test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ testGroup("Pasting", { template: "editor_empty" }, () => {
104104
delete window.unsanitized
105105
})
106106

107+
test("paste data-trix-attachment with markup in content", async () => {
108+
const content = "<style>p { color: red; }</style><p>a</p>"
109+
const attachment = JSON.stringify({ contentType: "text/html", content })
110+
await pasteContent("text/html", `<div data-trix-attachment='${attachment}'></div>`)
111+
112+
const attachments = getDocument().getAttachments()
113+
assert.equal(attachments.length, 1)
114+
assert.equal(attachments[0].getContent(), content)
115+
})
116+
107117
test("paste data-trix-attachment unsafe html", async () => {
108118
window.unsanitized = []
109119
const pasteData = {

src/test/unit/html_parser_test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,14 @@ testGroup("HTMLParser", () => {
294294
assert.documentHTMLEqual(HTMLParser.parse(html).getDocument(), expectedHTML)
295295
})
296296

297+
test("parses attachments whose content contains markup when sanitizing for XML", () => {
298+
const attachment = JSON.stringify({ contentType: "text/html", content: "<style>p { color: red; }</style><p>a</p>" })
299+
const html = `<div data-trix-attachment='${attachment}'></div>`
300+
const document = HTMLParser.parse(html, { purifyOptions: { SAFE_FOR_XML: true } }).getDocument()
301+
302+
assert.equal(document.getAttachmentPieces().length, 1)
303+
})
304+
297305
test("parses attachment caption from large html string", () => {
298306
let { html } = fixtures["image attachment with edited caption"]
299307

src/test/unit/html_sanitizer_test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,28 @@ testGroup("HTMLSanitizer", () => {
4040
})
4141
})
4242

43+
test("strips data-trix-serialized-attributes containing markup when sanitizing for XML", () => {
44+
const html = "<div data-trix-serialized-attributes='{\"a\":\"</style>\"}'>content</div>"
45+
const body = HTMLSanitizer.sanitize(html, { purifyOptions: { SAFE_FOR_XML: true } }).getBody()
46+
assert.notOk(body.innerHTML.includes("data-trix-serialized-attributes"))
47+
})
48+
49+
test("keeps Trix attributes containing markup when sanitizing for XML", () => {
50+
const markupValues = [ "</style>", "</title>", "</textarea>", "<![endif]-->", "]>" ]
51+
52+
markupValues.forEach((markup) => {
53+
const value = `{"contentType":"text/html","content":"${markup}"}`
54+
const html = `<figure data-trix-attachment='${value}'></figure>`
55+
const body = HTMLSanitizer.sanitize(html, { purifyOptions: { SAFE_FOR_XML: true } }).getBody()
56+
assert.equal(body.querySelector("figure").getAttribute("data-trix-attachment"), value)
57+
})
58+
})
59+
60+
test("removes other attributes containing markup when sanitizing for XML", () => {
61+
const html = "<a href=\"#\" class=\"</style>\">a</a>"
62+
const body = HTMLSanitizer.sanitize(html, { purifyOptions: { SAFE_FOR_XML: true } }).getBody()
63+
assert.equal(body.querySelector("a").hasAttribute("class"), false)
64+
})
4365
})
4466

4567
const withDOMPurifyConfig = (attrConfig = {}, fn) => {

src/trix/models/html_sanitizer.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,34 @@ import { nodeIsAttachmentElement, removeNode, tagName, walkTree } from "trix/cor
44
import DOMPurify from "dompurify"
55
import * as config from "trix/config"
66

7+
const ALLOWED_ATTRIBUTE_PATTERN = /^data-trix-/
8+
9+
// DOMPurify's SAFE_FOR_XML check drops attributes whose values contain markup before it
10+
// honors forceKeepAttr, so allowed attributes are stashed here and restored afterwards.
11+
let stashedAttributes = []
12+
713
DOMPurify.addHook("uponSanitizeAttribute", function (node, data) {
814
if (data.attrName === "data-trix-serialized-attributes") {
915
data.keepAttr = false
1016
return
1117
}
1218

13-
const allowedAttributePattern = /^data-trix-/
14-
if (allowedAttributePattern.test(data.attrName)) {
19+
if (ALLOWED_ATTRIBUTE_PATTERN.test(data.attrName)) {
1520
data.forceKeepAttr = true
21+
stashedAttributes.push([ data.attrName, node.getAttribute(data.attrName) ])
1622
}
1723
})
1824

25+
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
26+
stashedAttributes.forEach(([ name, value ]) => {
27+
if (value !== null && !node.hasAttribute(name)) {
28+
node.setAttribute(name, value)
29+
}
30+
})
31+
32+
stashedAttributes = []
33+
})
34+
1935
const DEFAULT_ALLOWED_ATTRIBUTES = "style href src width height language class".split(" ")
2036
const DEFAULT_FORBIDDEN_PROTOCOLS = "javascript:".split(" ")
2137
const DEFAULT_FORBIDDEN_ELEMENTS = "script iframe form noscript".split(" ")

0 commit comments

Comments
 (0)