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
  • Loading branch information
djmb authored Dec 11, 2024
2 parents 5db0ea4 + d910855 commit 32b0431
Show file tree
Hide file tree
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
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/test/system/pasting_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
`,
}

Expand All @@ -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
`,
}

Expand Down
1 change: 1 addition & 0 deletions src/test/unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
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
Expand Up @@ -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"
Expand Down
13 changes: 12 additions & 1 deletion src/trix/models/html_sanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(" ")
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 32b0431

Please sign in to comment.