From 05cb03c8abe8ab75dda06d1ac66d9ecc5fc4e253 Mon Sep 17 00:00:00 2001 From: Ethan Guo Date: Wed, 15 Jul 2026 20:19:46 +0800 Subject: [PATCH] fix(web): sandbox the speaker-notes presenter deck iframes (#5571) --- apps/web/src/runtime/speaker-notes.ts | 6 ++-- apps/web/tests/runtime/speaker-notes.test.ts | 32 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/apps/web/src/runtime/speaker-notes.ts b/apps/web/src/runtime/speaker-notes.ts index 1b45572191..544e1c3b0e 100644 --- a/apps/web/src/runtime/speaker-notes.ts +++ b/apps/web/src/runtime/speaker-notes.ts @@ -244,15 +244,15 @@ export function buildSpeakerNotesPresenterHtml(options: {
-
+
-
+
-
+
diff --git a/apps/web/tests/runtime/speaker-notes.test.ts b/apps/web/tests/runtime/speaker-notes.test.ts index 5384527ba3..8cf15912ad 100644 --- a/apps/web/tests/runtime/speaker-notes.test.ts +++ b/apps/web/tests/runtime/speaker-notes.test.ts @@ -372,4 +372,36 @@ describe('speaker notes HTML helpers', () => { expect(html).toContain('window.resizeTo(nextWidth, nextHeight)'); expect(html).toContain("window.addEventListener('resize', scheduleMinimumWindowSize)"); }); + + it('sandboxes the presenter slide iframes to an opaque origin', () => { + const html = buildSpeakerNotesPresenterHtml({ + previewHtml: '
One
', + title: 'Deck', + projectId: 'project-1', + fileName: 'deck.html', + notes: ['Intro'], + initialSlideIndex: 0, + slideCount: 1, + labels: { + title: 'Speaker notes', + edit: 'Edit', + save: 'Save notes', + pause: 'Pause', + resume: 'Resume', + reset: 'Reset', + previous: 'Previous', + next: 'Next', + empty: 'Empty', + slide: 'Slide {current} / {total}', + }, + }); + // The srcdoc loaded into each frame is the full, untrusted deck (scripts + // included); the frames must run it in an opaque origin so it cannot reach + // the app's storage or daemon. + for (const id of ['current', 'previous', 'next']) { + const tag = html.match(new RegExp(`