From 1c156905f8f282397fc4514dff59efdf41ae645e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 15 Apr 2025 15:44:13 +1000 Subject: [PATCH] Sanitize field content in editor The editor already strips script tags from fields, but was allowing through Javascript in things like onclick handlers. We block this now, as the editor context has access to internal APIs that we don't want to expose to untrusted third-party code. --- package.json | 1 + ts/editor/NoteEditor.svelte | 3 ++- ts/lib/domlib/index.ts | 1 + ts/lib/domlib/sanitize.ts | 10 ++++++++++ ts/licenses.json | 34 ++++++++++++++++++++++++++++------ yarn.lock | 20 ++++++++++++++++++++ 6 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 ts/lib/domlib/sanitize.ts diff --git a/package.json b/package.json index 1ce1353d5..76d1cd9a7 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "bootstrap-icons": "^1.10.5", "codemirror": "^5.63.1", "d3": "^7.0.0", + "dompurify": "^3.2.5", "fabric": "^5.3.0", "hammerjs": "^2.0.8", "intl-pluralrules": "^2.0.0", diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte index 9bf66d858..a51ef4fe9 100644 --- a/ts/editor/NoteEditor.svelte +++ b/ts/editor/NoteEditor.svelte @@ -133,7 +133,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } for (const [index, [, fieldContent]] of fs.entries()) { - fieldStores[index].set(fieldContent); + fieldStores[index].set(sanitize(fieldContent)); } fieldNames = newFieldNames; @@ -424,6 +424,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } from "../routes/image-occlusion/store"; import CollapseLabel from "./CollapseLabel.svelte"; import * as oldEditorAdapter from "./old-editor-adapter"; + import { sanitize } from "$lib/domlib"; $: isIOImageLoaded = false; $: ioImageLoadedStore.set(isIOImageLoaded); diff --git a/ts/lib/domlib/index.ts b/ts/lib/domlib/index.ts index 27b21016d..0834e0e77 100644 --- a/ts/lib/domlib/index.ts +++ b/ts/lib/domlib/index.ts @@ -5,4 +5,5 @@ export * from "./content-editable"; export * from "./location"; export * from "./move-nodes"; export * from "./place-caret"; +export * from "./sanitize"; export * from "./surround"; diff --git a/ts/lib/domlib/sanitize.ts b/ts/lib/domlib/sanitize.ts new file mode 100644 index 000000000..d6ae4f049 --- /dev/null +++ b/ts/lib/domlib/sanitize.ts @@ -0,0 +1,10 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import DOMPurify from "dompurify"; + +export function sanitize(html: string): string { + // We need to treat the text as a document fragment, or a style tag + // at the start of input will be discarded. + return DOMPurify.sanitize(html, { FORCE_BODY: true }); +} diff --git a/ts/licenses.json b/ts/licenses.json index 9d6f7d2c1..58815d397 100644 --- a/ts/licenses.json +++ b/ts/licenses.json @@ -57,6 +57,12 @@ "path": "node_modules/@tootallnate/once", "licenseFile": "node_modules/@tootallnate/once/LICENSE" }, + "@types/trusted-types@2.0.7": { + "licenses": "MIT", + "repository": "https://github.com/DefinitelyTyped/DefinitelyTyped", + "path": "node_modules/@types/trusted-types", + "licenseFile": "node_modules/@types/trusted-types/LICENSE" + }, "abab@2.0.6": { "licenses": "BSD-3-Clause", "repository": "https://github.com/jsdom/abab", @@ -95,8 +101,8 @@ "repository": "https://github.com/TooTallNate/node-agent-base", "publisher": "Nathan Rajlich", "email": "nathan@tootallnate.net", - "path": "node_modules/agent-base", - "licenseFile": "node_modules/agent-base/README.md" + "path": "node_modules/http-proxy-agent/node_modules/agent-base", + "licenseFile": "node_modules/http-proxy-agent/node_modules/agent-base/README.md" }, "asynckit@0.4.0": { "licenses": "MIT", @@ -436,6 +442,14 @@ "path": "node_modules/domexception", "licenseFile": "node_modules/domexception/LICENSE.txt" }, + "dompurify@3.2.5": { + "licenses": "(MPL-2.0 OR Apache-2.0)", + "repository": "https://github.com/cure53/DOMPurify", + "publisher": "Dr.-Ing. Mario Heiderich, Cure53", + "email": "mario@cure53.de", + "path": "node_modules/dompurify", + "licenseFile": "node_modules/dompurify/LICENSE" + }, "empty-npm-package@1.0.0": { "licenses": "ISC", "path": "node_modules/canvas" @@ -572,6 +586,14 @@ "path": "node_modules/lodash-es", "licenseFile": "node_modules/lodash-es/LICENSE" }, + "lru-cache@10.4.3": { + "licenses": "ISC", + "repository": "https://github.com/isaacs/node-lru-cache", + "publisher": "Isaac Z. Schlueter", + "email": "i@izs.me", + "path": "node_modules/lru-cache", + "licenseFile": "node_modules/lru-cache/LICENSE" + }, "marked@5.1.2": { "licenses": "MIT", "repository": "https://github.com/markedjs/marked", @@ -768,16 +790,16 @@ "repository": "https://github.com/jsdom/whatwg-url", "publisher": "Sebastian Mayr", "email": "github@smayr.name", - "path": "node_modules/whatwg-url", - "licenseFile": "node_modules/whatwg-url/LICENSE.txt" + "path": "node_modules/jsdom/node_modules/whatwg-url", + "licenseFile": "node_modules/jsdom/node_modules/whatwg-url/LICENSE.txt" }, "whatwg-url@11.0.0": { "licenses": "MIT", "repository": "https://github.com/jsdom/whatwg-url", "publisher": "Sebastian Mayr", "email": "github@smayr.name", - "path": "node_modules/data-urls/node_modules/whatwg-url", - "licenseFile": "node_modules/data-urls/node_modules/whatwg-url/LICENSE.txt" + "path": "node_modules/whatwg-url", + "licenseFile": "node_modules/whatwg-url/LICENSE.txt" }, "ws@8.18.0": { "licenses": "MIT", diff --git a/yarn.lock b/yarn.lock index a1ee4eed6..d52290ab2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1662,6 +1662,13 @@ __metadata: languageName: node linkType: hard +"@types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^5.60.1": version: 5.62.0 resolution: "@typescript-eslint/eslint-plugin@npm:5.62.0" @@ -2018,6 +2025,7 @@ __metadata: cross-env: "npm:^7.0.2" d3: "npm:^7.0.0" diff: "npm:^5.0.0" + dompurify: "npm:^3.2.5" dprint: "npm:^0.47.2" esbuild: "npm:^0.25.0" esbuild-sass-plugin: "npm:^2" @@ -3169,6 +3177,18 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:^3.2.5": + version: 3.2.5 + resolution: "dompurify@npm:3.2.5" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10c0/b564167cc588933ad2d25c185296716bdd7124e9d2a75dac76efea831bb22d1230ce5205a1ab6ce4c1010bb32ac35f7a5cb2dd16c78cbf382111f1228362aa59 + languageName: node + linkType: hard + "domutils@npm:^3.0.1": version: 3.1.0 resolution: "domutils@npm:3.1.0"