From 7fc979f95bba00a60d7cfb00d3f4e51dae671ea2 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:40:01 +0200 Subject: [PATCH] fix: skip embedded HTML parts and restrict link targets in docx preview (#29699) * fix: skip embedded HTML parts in docx preview The docx preview no longer renders altChunk parts, so an embedded HTML sub-document is omitted from the rendered output instead of being handed to the renderer. * fix: restrict docx preview link targets to safe schemes The docx preview kept whatever link target the document supplied, so a document could point a link at any scheme the browser understands. After rendering, a link target is now kept only when it resolves to http, https, mailto or tel. Anything else has its target removed and the link renders as plain text. Targets resolve against the page URL, so internal bookmark links and relative targets are unaffected, while an empty target, which the renderer emits for a hyperlink with no external relationship, is dropped instead of reloading the app. DOMPurify was not used because running it over the rendered document would strip the renderer's own markup and styling, so the check stays limited to link targets. Links using file: or Office application schemes no longer resolve. --- src/lib/components/common/DocxPreview.svelte | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/lib/components/common/DocxPreview.svelte b/src/lib/components/common/DocxPreview.svelte index 011420c171..eb49021be8 100644 --- a/src/lib/components/common/DocxPreview.svelte +++ b/src/lib/components/common/DocxPreview.svelte @@ -28,6 +28,8 @@ let zoomLevel = 1; let resizeObserver: ResizeObserver | null = null; + const SAFE_LINK_SCHEMES = ['http:', 'https:', 'mailto:', 'tel:']; + $: docxScale = Math.max(0.25, fitScale * zoomLevel); const clearPreview = () => { @@ -96,6 +98,24 @@ (pages[page - 1] as HTMLElement | undefined)?.scrollIntoView({ block: 'start' }); }; + const isSafeLinkTarget = (href: string) => { + if (!href.trim()) return false; + + try { + return SAFE_LINK_SCHEMES.includes(new URL(href, document.baseURI).protocol); + } catch { + return false; + } + }; + + const stripUnsafeLinkTargets = () => { + containerEl.querySelectorAll('a[href]').forEach((link) => { + if (!isSafeLinkTarget(link.getAttribute('href') ?? '')) { + link.removeAttribute('href'); + } + }); + }; + const renderDocx = async (arrayBuffer: ArrayBuffer | null) => { const currentRender = ++renderId; clearPreview(); @@ -116,12 +136,15 @@ className: 'docx', ignoreLastRenderedPageBreak: false, inWrapper: true, + // the renderer would otherwise inline a document-supplied HTML part into a same-origin frame + renderAltChunks: false, renderEndnotes: true, renderFooters: true, renderFootnotes: true, renderHeaders: true, useBase64URL: true }); + stripUnsafeLinkTargets(); await tick(); updateFitScale(); await scrollToTargetPage();