From a57978e739ca1dbcea01eb4a45be8e32616db3bb Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Mon, 4 May 2026 17:21:26 +0800 Subject: [PATCH 1/4] Harden QA markdown rendering to block WebView script injection Issue #221 requires closing the QA markdown XSS surface without broad CSP or architecture changes. This switches assistant and streaming markdown to one shared safe renderer that escapes raw HTML and rejects non-http(s)/mailto/tel links. Constraint: Keep markdown readability while preventing executable HTML in QA WebView Rejected: Add DOMPurify dependency | avoid new dependency for minimal fix Confidence: medium Scope-risk: narrow Directive: Keep streaming and final assistant rendering on the same sanitizer path Tested: npm run build (openless-all/app) Not-tested: Manual malicious payload interaction in packaged desktop app --- openless-all/app/src/lib/qaMarkdown.test.ts | 28 +++++++++++ openless-all/app/src/lib/qaMarkdown.ts | 51 +++++++++++++++++++++ openless-all/app/src/pages/QaPanel.tsx | 8 ++-- 3 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 openless-all/app/src/lib/qaMarkdown.test.ts create mode 100644 openless-all/app/src/lib/qaMarkdown.ts diff --git a/openless-all/app/src/lib/qaMarkdown.test.ts b/openless-all/app/src/lib/qaMarkdown.test.ts new file mode 100644 index 00000000..45628623 --- /dev/null +++ b/openless-all/app/src/lib/qaMarkdown.test.ts @@ -0,0 +1,28 @@ +import { renderQaMarkdown } from './qaMarkdown'; + +function assertIncludes(text: string, expected: string, name: string) { + if (!text.includes(expected)) { + throw new Error(`${name}: expected to include "${expected}", got "${text}"`); + } +} + +function assertNotIncludes(text: string, expected: string, name: string) { + if (text.includes(expected)) { + throw new Error(`${name}: expected not to include "${expected}", got "${text}"`); + } +} + +const htmlEscaped = renderQaMarkdown(''); +assertIncludes(htmlEscaped, '<img src=x onerror=alert(1)>', 'raw html should be escaped'); +assertNotIncludes(htmlEscaped, ''); assertIncludes(htmlEscaped, '<img src=x onerror=alert(1)>', 'raw html should be escaped'); assertNotIncludes(htmlEscaped, '