fix harden preview against self XSS

Signed-off-by: RonniSkansing <rskansing@gmail.com>
This commit is contained in:
RonniSkansing committed 2026-10-09 10:58:02 +02:00
1 parent e6f5e20a61
commit 67cc91d30d
19 files changed
+421 -118

No files matched your search

@@ -158,6 +158,7 @@
import * as vimModule from 'monaco-vim';
import { BiMap } from '$lib/utils/maps';
import { previewQR as generateQR } from '$lib/utils/qrPreview';
import { submitPreviewForm } from '$lib/utils/safePreview.js';
import { vimModeEnabled } from '$lib/store/vimMode.js';
import {
setupVimClipboardIntegration,
@@ -176,6 +177,8 @@
let localVimMode = externalVimMode !== null ? externalVimMode : $vimModeEnabled;
let editor = null;
let previewFrame = null;
// unique name so the preview form can target this editor's iframe
let previewFrameName = 'pc-preview-' + Math.random().toString(36).slice(2);
let previewRenderDelayID = null;
let previewRenderDelay = 250;
let isRenderingPreview = false;
@@ -183,7 +186,6 @@
let previousQRHash = 0;
let isPreviewVisible = false;
let externalFrameRef = null;
let fileInputRef;
let shadowContainer = null;
let vimStatusBar = null;
@@ -730,42 +732,23 @@
updatePreview();
};
// create shadow dom for iframe isolation
const createShadowIframe = () => {
// create the named iframe the preview form posts into. the sandboxed server
// response is what isolates the content; the sandbox attribute without
// allow-same-origin adds a second layer. a plain iframe, not a shadow root,
// so the form can find the frame by name.
const createPreviewIframe = () => {
if (!shadowContainer) return;
// clear existing content
shadowContainer.innerHTML = '';
// create shadow root
const shadowRoot = shadowContainer.attachShadow({ mode: 'closed' });
// create iframe inside shadow dom
const iframe = document.createElement('iframe');
iframe.sandbox = 'allow-forms allow-modals allow-popups allow-scripts allow-pointer-lock';
iframe.name = previewFrameName;
iframe.sandbox = 'allow-forms allow-modals allow-popups allow-scripts';
iframe.title = 'preview';
iframe.style.cssText = 'height: 100%; width: 100%; border: none;';
iframe.style.cssText = 'height: 100%; width: 100%; border: none; background: white;';
// add styles to shadow root to isolate it
const style = document.createElement('style');
style.textContent = `
:host {
display: block;
height: 100%;
width: 100%;
background: white;
}
iframe {
height: 100%;
width: 100%;
border: none;
}
`;
shadowContainer.appendChild(iframe);
shadowRoot.appendChild(style);
shadowRoot.appendChild(iframe);
// set as preview frame
previewFrame = iframe;
};
@@ -777,19 +760,15 @@
value = v;
const content = await replaceTemplateVariables(v);
// create shadow iframe if not exists
// create the preview iframe if not exists
if (shadowContainer && !previewFrame) {
createShadowIframe();
createPreviewIframe();
}
// post the content to the backend, which returns it with a sandbox policy
// so the preview runs isolated and still loads its own external resources
if (previewFrame) {
// use data url for null origin isolation
previewFrame.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(content);
}
if (externalFrameRef) {
const embedContent = createEmbed(content);
const dataUrl = 'data:text/html;charset=utf-8,' + encodeURIComponent(embedContent);
externalFrameRef.location.replace(dataUrl);
submitPreviewForm(content, previewFrameName);
}
isRenderingPreview = false;
};
@@ -1006,33 +985,14 @@
reader.readAsText(file);
};
const createEmbed = (content) => {
return `
<!DOCTYPE html>
<html>
<head>
<title></title>
<style>
*, body, iframe {margin: 0; padding: 0; border: 0; height: 100%; width: 100%;}
</style>
</head>
<body>
<iframe
sandbox="allow-forms allow-modals allow-popups allow-scripts allow-pointer-lock"
src="data:text/html;charset=utf-8,${encodeURIComponent(content)}"></iframe>
</body>
</html>
`;
};
const openFullPagePreview = async (e) => {
e.preventDefault();
const v = editor.getValue();
value = v;
const content = await replaceTemplateVariables(v);
const blob = new Blob([createEmbed(content)], { type: 'text/html' });
let url = URL.createObjectURL(blob);
externalFrameRef = window.open(url, '_blank');
// open the current content isolated in a new tab through the backend so it
// gets its own sandbox policy and loads its own external resources
submitPreviewForm(content, '_blank');
};
const triggerFileInput = () => {