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: '',
+ 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(`