From 272c7e27e722608732a67108ad3fe7870e233ac8 Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Mon, 9 Dec 2024 09:25:43 +0000 Subject: [PATCH] Sanitize HTML with DOMPurify Sanitize HTML with DOMPurify to prevent XSS attacks. This is a fix for two security vulnerabilities for MathML and embed based bypasses when pasting data into a Trix editor. --- package.json | 3 +++ rollup.config.js | 2 +- src/test/system/pasting_test.js | 30 ++++++++++++++++++++++++++++++ src/trix/models/html_sanitizer.js | 4 +++- yarn.lock | 5 +++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b3d9b1c00..4002b0009 100644 --- a/package.json +++ b/package.json @@ -68,5 +68,8 @@ "postrelease": "git push && git push --tags", "dev": "web-dev-server --app-index index.html --root-dir dist --node-resolve --open", "start": "yarn build-assets && concurrently --kill-others --names js,css,dev-server 'yarn watch' 'yarn build-css --watch' 'yarn dev'" + }, + "dependencies": { + "dompurify": "^3.2.0" } } diff --git a/rollup.config.js b/rollup.config.js index a8d5b2796..beadf1056 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -41,7 +41,7 @@ const terserConfig = terser({ const type = comment.type if (type == "comment2") { // multiline comment - return /Copyright/.test(text) + return /@license|Copyright/.test(text) } }, }, diff --git a/src/test/system/pasting_test.js b/src/test/system/pasting_test.js index 492ac0927..f3ddbf7fd 100644 --- a/src/test/system/pasting_test.js +++ b/src/test/system/pasting_test.js @@ -119,6 +119,36 @@ testGroup("Pasting", { template: "editor_empty" }, () => { delete window.unsanitized }) + test("paste data-trix-attachment encoded mathml", async () => { + window.unsanitized = [] + const pasteData = { + "text/plain": "x", + "text/html": `\ + copy
me + `, + } + + await pasteContent(pasteData) + await delay(20) + assert.deepEqual(window.unsanitized, []) + delete window.unsanitized + }) + + test("paste data-trix-attachment encoded embed", async () => { + window.unsanitized = [] + const pasteData = { + "text/plain": "x", + "text/html": `\ + copy
me + `, + } + + await pasteContent(pasteData) + await delay(20) + assert.deepEqual(window.unsanitized, []) + delete window.unsanitized + }) + test("prefers plain text when html lacks formatting", async () => { const pasteData = { "text/html": "a\nb", diff --git a/src/trix/models/html_sanitizer.js b/src/trix/models/html_sanitizer.js index 3dd0b3e68..4e93e7011 100644 --- a/src/trix/models/html_sanitizer.js +++ b/src/trix/models/html_sanitizer.js @@ -1,6 +1,7 @@ import BasicObject from "trix/core/basic_object" import { nodeIsAttachmentElement, removeNode, tagName, walkTree } from "trix/core/helpers" +import DOMPurify from "dompurify" const DEFAULT_ALLOWED_ATTRIBUTES = "style href src width height language class".split(" ") const DEFAULT_FORBIDDEN_PROTOCOLS = "javascript:".split(" ") @@ -29,7 +30,8 @@ export default class HTMLSanitizer extends BasicObject { sanitize() { this.sanitizeElements() - return this.normalizeListElementNesting() + this.normalizeListElementNesting() + return DOMPurify.sanitize(this.body, { ADD_ATTR: [ "language" ], RETURN_DOM: true } ) } getHTML() { diff --git a/yarn.lock b/yarn.lock index 65a806069..37a871b5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2829,6 +2829,11 @@ domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +dompurify@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.0.tgz#53c414317c51503183696fcdef6dd3f916c607ed" + integrity sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ== + domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"