mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix: fix link handling in example preview iframe sandbox (#701)
* fix: fix link handling in example preview iframe sandbox * fix: added missing single quote * fix(srcdoc): security/correctness issues - Use location.hash = href for hash-link navigation so hashchange events, CSS :target, history/back and named anchors work correctly - Always pass 'noopener,noreferrer' to window.open() for _blank links - Guard e.target with instanceof Element before calling .closest() to avoid TypeError on text node click targets * fix(srcdoc): security/correctness issues - Added a protocol allowlist - Re-navigation to same hash - Consistency with rest of shim IIFE using var instead of let and const * fix(srcdoc): scroll to top on bare hash links (#) Intercept href="#" clicks and scroll to top of page in the iframe sandbox to mimic native browser behaviour. * test(web): update PreviewModal sandbox test assertions to include popup flags
This commit is contained in:
@@ -417,7 +417,7 @@ export function PreviewModal({
|
||||
<iframe
|
||||
key={activeView?.id ?? 'view'}
|
||||
title={`${title} ${activeView?.label ?? ''}`}
|
||||
sandbox="allow-scripts"
|
||||
sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"
|
||||
srcDoc={srcDoc}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -169,6 +169,10 @@ function escapeAttr(value: string): string {
|
||||
// becomes a static, unnavigable preview. We install a same-origin
|
||||
// in-memory shim BEFORE any user script runs so those decks degrade
|
||||
// gracefully (position just doesn't persist across reloads).
|
||||
// allow-popups and allow-popups-to-escape-sandbox are needed for
|
||||
// links with target="_blank" to work in the sandboxed preview.
|
||||
// Empty hrefs and hash only hrefs will be intercepted and ignored.
|
||||
// hrefs leading to an id on the page will be scrolled into view.
|
||||
function injectSandboxShim(doc: string): string {
|
||||
const shim = `<script data-od-sandbox-shim>(function(){
|
||||
function makeStore(){
|
||||
@@ -193,6 +197,40 @@ function injectSandboxShim(doc: string): string {
|
||||
}
|
||||
tryShim('localStorage');
|
||||
tryShim('sessionStorage');
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target || !(e.target instanceof Element)) return;
|
||||
var link = e.target.closest('a[href]');
|
||||
if (!link) return;
|
||||
var href = link.getAttribute('href');
|
||||
if (href === null) return;
|
||||
var isAnchor = href.startsWith('#') || href === '';
|
||||
if (isAnchor) {
|
||||
e.preventDefault();
|
||||
if (href === '' || href === '#') {
|
||||
window.scrollTo({ top: 0 });
|
||||
history.replaceState(null, '', ' ');
|
||||
} else {
|
||||
var targetId = href.slice(1);
|
||||
var target = targetId ? document.getElementById(targetId) : null;
|
||||
if (target) {
|
||||
target.scrollIntoView();
|
||||
location.hash === href && history.replaceState(null, '', ' ');
|
||||
location.hash = href;
|
||||
}
|
||||
}
|
||||
} else if (link.getAttribute('target') === '_blank') {
|
||||
e.preventDefault();
|
||||
let safe = false;
|
||||
try {
|
||||
var url = new URL(href, location.href);
|
||||
safe =
|
||||
url.protocol === 'http:' ||
|
||||
url.protocol === 'https:' ||
|
||||
url.protocol === 'mailto:';
|
||||
} catch (_) {}
|
||||
safe && window.open(href, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
});
|
||||
})();</script>`;
|
||||
if (/<head[^>]*>/i.test(doc))
|
||||
return doc.replace(/<head[^>]*>/i, (m) => `${m}${shim}`);
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('PreviewModal sandbox isolation', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('sandbox="allow-scripts"');
|
||||
expect(markup).toContain('sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"');
|
||||
expect(markup).not.toContain('allow-same-origin');
|
||||
expect(markup).toContain('srcDoc=');
|
||||
});
|
||||
@@ -42,8 +42,28 @@ describe('PreviewModal sandbox isolation', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('sandbox="allow-scripts"');
|
||||
expect(markup).toContain('sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"');
|
||||
expect(markup).not.toContain('allow-same-origin');
|
||||
expect(markup).toContain('od:slide');
|
||||
});
|
||||
|
||||
it('includes popup flags in the sandbox attribute', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<PreviewModal
|
||||
title="Popup preview"
|
||||
views={[
|
||||
{
|
||||
id: 'popup',
|
||||
label: 'Popup',
|
||||
html: '<button onclick="window.open(\'https://example.com\')">Open Popup</button>',
|
||||
},
|
||||
]}
|
||||
exportTitleFor={() => 'popup-preview'}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('allow-popups');
|
||||
expect(markup).toContain('allow-popups-to-escape-sandbox');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user