diff --git a/README.md b/README.md index c4008b844..f1a5196c4 100644 --- a/README.md +++ b/README.md @@ -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 `` element emits several events which you can use to observe and respond to changes in editor state. diff --git a/src/test/system/pasting_test.js b/src/test/system/pasting_test.js index f3ddbf7fd..5ff63ad1a 100644 --- a/src/test/system/pasting_test.js +++ b/src/test/system/pasting_test.js @@ -124,7 +124,7 @@ testGroup("Pasting", { template: "editor_empty" }, () => { const pasteData = { "text/plain": "x", "text/html": `\ - copy
me + copy
me `, } @@ -139,7 +139,7 @@ testGroup("Pasting", { template: "editor_empty" }, () => { const pasteData = { "text/plain": "x", "text/html": `\ - copy
me + copy
me `, } diff --git a/src/test/unit.js b/src/test/unit.js index 81da21ac8..31dd25776 100644 --- a/src/test/unit.js +++ b/src/test/unit.js @@ -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" diff --git a/src/test/unit/html_sanitizer_test.js b/src/test/unit/html_sanitizer_test.js new file mode 100644 index 000000000..16994cce8 --- /dev/null +++ b/src/test/unit/html_sanitizer_test.js @@ -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 = "" + 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 = "" + const expectedHTML = "" + 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) + } +} diff --git a/src/trix/config/dompurify.js b/src/trix/config/dompurify.js new file mode 100644 index 000000000..81889fc5e --- /dev/null +++ b/src/trix/config/dompurify.js @@ -0,0 +1,4 @@ +export default { + ADD_ATTR: [ "language" ], + RETURN_DOM: true +} diff --git a/src/trix/config/index.js b/src/trix/config/index.js index 27f7abc2f..5556ec88b 100644 --- a/src/trix/config/index.js +++ b/src/trix/config/index.js @@ -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" diff --git a/src/trix/models/html_sanitizer.js b/src/trix/models/html_sanitizer.js index 4e93e7011..98360449c 100644 --- a/src/trix/models/html_sanitizer.js +++ b/src/trix/models/html_sanitizer.js @@ -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() {