From 29e7f9cc640a751baab775f27c833e2f1c304bee Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 1 Sep 2026 11:27:27 -0700 Subject: [PATCH 1/5] Sanitize HTML in our VS Code panels ...and add CSP for added protection. --- source/vscode/src/webview/help.tsx | 2 +- source/vscode/src/webview/webview.tsx | 5 +++-- source/vscode/src/webviewPanel.ts | 9 +++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/source/vscode/src/webview/help.tsx b/source/vscode/src/webview/help.tsx index 24718a3d54e..b140f1e0678 100644 --- a/source/vscode/src/webview/help.tsx +++ b/source/vscode/src/webview/help.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef } from "preact/hooks"; -declare const resourcesUri: string; // Set by the HTML in the window +const resourcesUri = document.body.dataset.resourcesUri ?? ""; let svgPromise: Promise; diff --git a/source/vscode/src/webview/webview.tsx b/source/vscode/src/webview/webview.tsx index 23b8ed3ced4..3c1518673a9 100644 --- a/source/vscode/src/webview/webview.tsx +++ b/source/vscode/src/webview/webview.tsx @@ -20,16 +20,17 @@ import { HelpPage } from "./help"; import { DocumentationView, IDocFile } from "./docview"; import "./webview.css"; +import DOMPurify from "dompurify"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - there are no types for this import mk from "@vscode/markdown-it-katex"; import markdownIt from "markdown-it"; -const md = markdownIt("commonmark"); +const md = markdownIt("commonmark", { html: false }); md.use(mk, { enableMathBlockInHtml: true, enableMathInlineInHtml: true, }); -setRenderer((input: string) => md.render(input)); +setRenderer((input: string) => DOMPurify.sanitize(md.render(input))); window.addEventListener("message", onMessage); window.addEventListener("load", main); diff --git a/source/vscode/src/webviewPanel.ts b/source/vscode/src/webviewPanel.ts index e7f4c199378..2c0e5cc3e3a 100644 --- a/source/vscode/src/webviewPanel.ts +++ b/source/vscode/src/webviewPanel.ts @@ -336,23 +336,24 @@ export class QSharpWebViewPanel { const webviewCss = getUri(["out", "webview", "webview.css"]); const webviewJs = getUri(["out", "webview", "webview.js"]); const resourcesUri = getUri(["resources"]); + const cspSource = webview.cspSource; return /*html*/ ` + Q# - - + + `; From 22aaa389f9509f886122adadecb1c9715a6e916d Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 1 Sep 2026 11:37:12 -0700 Subject: [PATCH 2/5] Sanitize HTML in the playground as well This is overbroad, but the trusted content appearing in places other than the doc panel should already be sanitized, so this should be a no-op. --- source/playground/src/main.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/playground/src/main.tsx b/source/playground/src/main.tsx index 1c712cff924..ce7cc3fc6b2 100644 --- a/source/playground/src/main.tsx +++ b/source/playground/src/main.tsx @@ -58,12 +58,13 @@ import { decodeGatesUrl, } from "qsharp-lang/ux"; -const md = markdownIt("commonmark"); +import DOMPurify from "dompurify"; +const md = markdownIt("commonmark", { html: false }); md.use((mk as any).default, { enableMathBlockInHtml: true, enableMathInlineInHtml: true, }); // Not sure why it's not using the default export automatically :-/ -setRenderer((input: string) => md.render(input)); +setRenderer((input: string) => DOMPurify.sanitize(md.render(input))); export type ActiveTab = | "results-tab" From 21fa7987258d241425ef829acbe323061217448c Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 1 Sep 2026 12:42:41 -0700 Subject: [PATCH 3/5] Fix help panel --- source/vscode/src/webview/help.tsx | 3 +-- source/vscode/src/webviewPanel.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/source/vscode/src/webview/help.tsx b/source/vscode/src/webview/help.tsx index b140f1e0678..66ab365234c 100644 --- a/source/vscode/src/webview/help.tsx +++ b/source/vscode/src/webview/help.tsx @@ -3,8 +3,6 @@ import { useEffect, useRef } from "preact/hooks"; -const resourcesUri = document.body.dataset.resourcesUri ?? ""; - let svgPromise: Promise; export function HelpPage() { @@ -14,6 +12,7 @@ export function HelpPage() { // Ensure that the fetch is kicked off once for the module if (!svgPromise) { + const resourcesUri = document.body.dataset.resourcesUri ?? ""; svgPromise = fetch(`${resourcesUri}/DebugDropDown.svg`); } diff --git a/source/vscode/src/webviewPanel.ts b/source/vscode/src/webviewPanel.ts index 2c0e5cc3e3a..3186aefe244 100644 --- a/source/vscode/src/webviewPanel.ts +++ b/source/vscode/src/webviewPanel.ts @@ -344,7 +344,7 @@ export class QSharpWebViewPanel { + content="default-src 'none'; img-src ${cspSource}; style-src ${cspSource} 'unsafe-inline'; font-src ${cspSource}; script-src ${cspSource}; connect-src ${cspSource};" /> Q# From 5034d75ed4eb6447b17bf3e213a96909e2c1d966 Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Tue, 1 Sep 2026 12:57:50 -0700 Subject: [PATCH 4/5] Allow xref links --- katas/content/linear_algebra/index.md | 2 +- source/playground/src/main.tsx | 7 ++++++- source/vscode/src/webview/webview.tsx | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/katas/content/linear_algebra/index.md b/katas/content/linear_algebra/index.md index db4cf78e9dd..61c5991dee9 100644 --- a/katas/content/linear_algebra/index.md +++ b/katas/content/linear_algebra/index.md @@ -219,7 +219,7 @@ A square matrix has a property called the **determinant**, with the determinant For a $2 \times 2$ matrix $A$, the determinant is defined as $|A| = A_{0,0} \cdot A_{1,1} - A_{0,1} \cdot A_{1,0}$. -For larger matrices, the determinant is defined through determinants of sub-matrices. You can learn more about the determinant of matrices from [Wikipedia](https://en.wikipedia.org/wiki/Determinant) or from [Wolfram MathWorld](http://mathworld.wolfram.com/Determinant.html). +For larger matrices, the determinant is defined through determinants of sub-matrices. You can learn more about the determinant of matrices from [Wikipedia](https://en.wikipedia.org/wiki/Determinant) or from [Wolfram MathWorld](https://mathworld.wolfram.com/Determinant.html). @[exercise]({ "id": "linear_algebra__inverse_matrix_ex", diff --git a/source/playground/src/main.tsx b/source/playground/src/main.tsx index ce7cc3fc6b2..c1957705fb7 100644 --- a/source/playground/src/main.tsx +++ b/source/playground/src/main.tsx @@ -64,7 +64,12 @@ md.use((mk as any).default, { enableMathBlockInHtml: true, enableMathInlineInHtml: true, }); // Not sure why it's not using the default export automatically :-/ -setRenderer((input: string) => DOMPurify.sanitize(md.render(input))); +// Allow only the protocols used in doc/kata/estimator content +// Borrowed from DOMPurify and filtered to our protocols +const ALLOWED_URI = /^(?:(?:https|xref):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i; +setRenderer((input: string) => + DOMPurify.sanitize(md.render(input), { ALLOWED_URI_REGEXP: ALLOWED_URI }), +); export type ActiveTab = | "results-tab" diff --git a/source/vscode/src/webview/webview.tsx b/source/vscode/src/webview/webview.tsx index 3c1518673a9..5ec3b5bc3b9 100644 --- a/source/vscode/src/webview/webview.tsx +++ b/source/vscode/src/webview/webview.tsx @@ -30,7 +30,12 @@ md.use(mk, { enableMathBlockInHtml: true, enableMathInlineInHtml: true, }); -setRenderer((input: string) => DOMPurify.sanitize(md.render(input))); +// Allow only the protocols used in doc/kata/estimator content +// Borrowed from DOMPurify and filtered to our protocols +const ALLOWED_URI = /^(?:(?:https|xref):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i; +setRenderer((input: string) => + DOMPurify.sanitize(md.render(input), { ALLOWED_URI_REGEXP: ALLOWED_URI }), +); window.addEventListener("message", onMessage); window.addEventListener("load", main); From 7ad9f7162b23101121af1a2e3a80758d32e70e3f Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Thu, 3 Sep 2026 11:50:12 -0700 Subject: [PATCH 5/5] Unbreak katas in the playground --- source/playground/src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/playground/src/main.tsx b/source/playground/src/main.tsx index c1957705fb7..b40fcc5c66f 100644 --- a/source/playground/src/main.tsx +++ b/source/playground/src/main.tsx @@ -59,7 +59,7 @@ import { } from "qsharp-lang/ux"; import DOMPurify from "dompurify"; -const md = markdownIt("commonmark", { html: false }); +const md = markdownIt("commonmark"); md.use((mk as any).default, { enableMathBlockInHtml: true, enableMathInlineInHtml: true,