Skip to content

Commit

Permalink
Merge pull request #1208 from basecamp/dom-purify-config
Browse files Browse the repository at this point in the history
Make DOMPurify configurable
djmb authored Dec 11, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 5db0ea4 + d910855 commit 32b0431
Showing 7 changed files with 84 additions and 3 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -548,6 +548,16 @@ localStorage["editorState"] = JSON.stringify(element.editor)
element.editor.loadJSON(JSON.parse(localStorage["editorState"]))
```
## HTML Sanitization
Trix uses [DOMPurify](https://github.com/cure53/DOMPurify/) to sanitize the editor content. You can set the DOMPurify config via `Trix.config.dompurify`.
For example if you want to keep a custom tag, you can access do that with:
```js
Trix.config.dompurify.ADD_TAGS = [ "my-custom-tag" ]
```
## Observing Editor Changes
The `<trix-editor>` element emits several events which you can use to observe and respond to changes in editor state.
4 changes: 2 additions & 2 deletions src/test/system/pasting_test.js
Original file line number Diff line number Diff line change
@@ -124,7 +124,7 @@ testGroup("Pasting", { template: "editor_empty" }, () => {
const pasteData = {
"text/plain": "x",
"text/html": `\
copy<div data-trix-attachment="{&quot;contentType&quot;:&quot;text/html5&quot;,&quot;content&quot;:&quot;&lt;math&gt;&lt;mtext&gt;&lt;table&gt;&lt;mglyph&gt;&lt;style&gt;&lt;img src=x onerror=alert()&gt;&lt;/style&gt;XSS POC&quot;}"></div>me
copy<div data-trix-attachment="{&quot;contentType&quot;:&quot;text/html5&quot;,&quot;content&quot;:&quot;&lt;math&gt;&lt;mtext&gt;&lt;table&gt;&lt;mglyph&gt;&lt;style&gt;&lt;img src=x onerror=window.unsanitized.push(1)&gt;&lt;/style&gt;XSS POC&quot;}"></div>me
`,
}

@@ -139,7 +139,7 @@ testGroup("Pasting", { template: "editor_empty" }, () => {
const pasteData = {
"text/plain": "x",
"text/html": `\
copy<div data-trix-attachment="{&quot;contentType&quot;:&quot;text/html5&quot;,&quot;content&quot;:&quot;&lt;embed src='javascript:alert(1)'&gt;XSS POC&quot;}"></div>me
copy<div data-trix-attachment="{&quot;contentType&quot;:&quot;text/html5&quot;,&quot;content&quot;:&quot;&lt;embed src='window.unsanitized.push(1)'&gt;XSS POC&quot;}"></div>me
`,
}

1 change: 1 addition & 0 deletions src/test/unit.js
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import "test/unit/composition_test"
import "test/unit/document_test"
import "test/unit/document_view_test"
import "test/unit/html_parser_test"
import "test/unit/html_sanitizer_test"
import "test/unit/location_mapper_test"
import "test/unit/mutation_observer_test"
import "test/unit/serialization_test"
54 changes: 54 additions & 0 deletions src/test/unit/html_sanitizer_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
assert,
test,
testGroup,
} from "test/test_helper"

import { HTMLSanitizer } from "../../trix/models"
import * as config from "../../trix/config"

testGroup("HTMLSanitizer", () => {
test("strips custom tags", () => {
const html = "<custom-tag></custom-tag>"
const expectedHTML = ""
const document = HTMLSanitizer.sanitize(html).body.innerHTML
assert.equal(document, expectedHTML)
})

test("keeps custom tags configured for DOMPurify", () => {
const config = {
ADD_TAGS: [ "custom-tag" ],
RETURN_DOM: true,
}
withDOMPurifyConfig(config, () => {
const html = "<custom-tag></custom-tag>"
const expectedHTML = "<custom-tag></custom-tag>"
const document = HTMLSanitizer.sanitize(html).body.innerHTML
assert.equal(document, expectedHTML)
})
})
})

const withDOMPurifyConfig = (attrConfig = {}, fn) => {
withConfig("dompurify", attrConfig, fn)
}

const withConfig = (section, newConfig = {}, fn) => {
const originalConfig = Object.assign({}, config[section])
const copy = (section, properties) => {
for (const [ key, value ] of Object.entries(properties)) {
if (value) {
config[section][key] = value
} else {
delete config[section][key]
}
}
}

try {
copy(section, newConfig)
fn()
} finally {
copy(section, originalConfig)
}
}
4 changes: 4 additions & 0 deletions src/trix/config/dompurify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
ADD_ATTR: [ "language" ],
RETURN_DOM: true
}
1 change: 1 addition & 0 deletions src/trix/config/index.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ export { default as attachments } from "./attachments"
export { default as blockAttributes } from "./block_attributes"
export { default as browser } from "./browser"
export { default as css } from "./css"
export { default as dompurify } from "./dompurify"
export { default as fileSize } from "./file_size_formatting"
export { default as input } from "./input"
export { default as keyNames } from "./key_names"
13 changes: 12 additions & 1 deletion src/trix/models/html_sanitizer.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,14 @@ import BasicObject from "trix/core/basic_object"

import { nodeIsAttachmentElement, removeNode, tagName, walkTree } from "trix/core/helpers"
import DOMPurify from "dompurify"
import * as config from "trix/config"

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

const DEFAULT_ALLOWED_ATTRIBUTES = "style href src width height language class".split(" ")
const DEFAULT_FORBIDDEN_PROTOCOLS = "javascript:".split(" ")
@@ -31,7 +39,10 @@ export default class HTMLSanitizer extends BasicObject {
sanitize() {
this.sanitizeElements()
this.normalizeListElementNesting()
return DOMPurify.sanitize(this.body, { ADD_ATTR: [ "language" ], RETURN_DOM: true } )
DOMPurify.setConfig(config.dompurify)
this.body = DOMPurify.sanitize(this.body)

return this.body
}

getHTML() {

0 comments on commit 32b0431

Please sign in to comment.