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.
This commit is contained in:
Classic298 2026-09-06 22:40:01 +02:00 committed by GitHub
parent 08ca3d859f
commit 7fc979f95b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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();