Use CSP to block inline JS content in editor (#3939)

* Revert "Sanitize field content in editor"

This reverts commit 1c156905f8.

* Use CSP to block inline JS content in editor

This blocks inline scripts, scripts in the media folder, and
handlers like onclick in the editor. This is nicer than the previous
solution - it doesn't make any permanent changes, and leaves other
content like SVGs alone. Thanks to Nil Admirari for the suggestion.

(cherry picked from commit ddb8573e8d)
This commit is contained in:
Damien Elmes 2025-04-23 16:21:48 +10:00 committed by Damien Elmes
parent e9dfb7a13d
commit bfc87c0427
7 changed files with 23 additions and 66 deletions

View file

@ -69,7 +69,6 @@
"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",

View file

@ -141,14 +141,17 @@ class MediaServer(threading.Thread):
) -> None:
self._legacy_pages[id] = LegacyPage(html, context)
def get_page(self, id: int) -> LegacyPage | None:
return self._legacy_pages.get(id)
def get_page_html(self, id: int) -> str | None:
if page := self._legacy_pages.get(id):
if page := self.get_page(id):
return page.html
else:
return None
def get_page_context(self, id: int) -> PageContext | None:
if page := self._legacy_pages.get(id):
if page := self.get_page(id):
return page.context
else:
return None
@ -740,8 +743,17 @@ def _handle_dynamic_request(req: DynamicRequest) -> Response:
def legacy_page_data() -> Response:
id = int(request.args["id"])
if html := aqt.mw.mediaServer.get_page_html(id):
return Response(html, mimetype="text/html")
page = aqt.mw.mediaServer.get_page(id)
if page:
response = Response(page.html, mimetype="text/html")
# Prevent JS in field content from being executed in the editor, as it would
# have access to our internal API, and is a security risk.
if page.context == PageContext.EDITOR:
port = aqt.mw.mediaServer.getPort()
response.headers["Content-Security-Policy"] = (
f"script-src http://127.0.0.1:{port}/_anki/"
)
return response
else:
return _text_response(HTTPStatus.NOT_FOUND, "page not found")

View file

@ -132,7 +132,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(sanitize(fieldContent));
fieldStores[index].set(fieldContent);
}
fieldNames = newFieldNames;
@ -413,7 +413,6 @@ 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);

View file

@ -5,5 +5,4 @@ export * from "./content-editable";
export * from "./location";
export * from "./move-nodes";
export * from "./place-caret";
export * from "./sanitize";
export * from "./surround";

View file

@ -1,10 +0,0 @@
// 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 });
}

View file

@ -57,12 +57,6 @@
"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",
@ -101,8 +95,8 @@
"repository": "https://github.com/TooTallNate/node-agent-base",
"publisher": "Nathan Rajlich",
"email": "nathan@tootallnate.net",
"path": "node_modules/http-proxy-agent/node_modules/agent-base",
"licenseFile": "node_modules/http-proxy-agent/node_modules/agent-base/README.md"
"path": "node_modules/agent-base",
"licenseFile": "node_modules/agent-base/README.md"
},
"asynckit@0.4.0": {
"licenses": "MIT",
@ -442,14 +436,6 @@
"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"
@ -586,14 +572,6 @@
"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",
@ -790,16 +768,16 @@
"repository": "https://github.com/jsdom/whatwg-url",
"publisher": "Sebastian Mayr",
"email": "github@smayr.name",
"path": "node_modules/jsdom/node_modules/whatwg-url",
"licenseFile": "node_modules/jsdom/node_modules/whatwg-url/LICENSE.txt"
"path": "node_modules/whatwg-url",
"licenseFile": "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/whatwg-url",
"licenseFile": "node_modules/whatwg-url/LICENSE.txt"
"path": "node_modules/data-urls/node_modules/whatwg-url",
"licenseFile": "node_modules/data-urls/node_modules/whatwg-url/LICENSE.txt"
},
"ws@8.18.0": {
"licenses": "MIT",

View file

@ -1648,13 +1648,6 @@ __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"
@ -2011,7 +2004,6 @@ __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.19.10"
esbuild-sass-plugin: "npm:^2"
@ -3162,18 +3154,6 @@ __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"