From da9d99924cd29c47d559fca440133d39240b094a Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 10 Dec 2024 11:49:15 +0000 Subject: [PATCH 1/2] Make DOMPurify configurable Expose Trix.config.dompurify to allow users to configure DOMPurify. Additionally avoid stripping data-trix-* attributes. --- README.md | 10 ++++++ src/test/unit.js | 1 + src/test/unit/html_sanitizer_test.js | 54 ++++++++++++++++++++++++++++ src/trix/config/dompurify.js | 4 +++ src/trix/config/index.js | 1 + src/trix/models/html_sanitizer.js | 13 ++++++- 6 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/test/unit/html_sanitizer_test.js create mode 100644 src/trix/config/dompurify.js 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/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() { From d9108550178279073b9a59375b1f5638771ebfa0 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Wed, 11 Dec 2024 11:54:52 +0000 Subject: [PATCH 2/2] Fix sanitization checks --- src/test/system/pasting_test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 `, }