From 2cd1baa594639e37b4e66b7906fc136cb4a10147 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:07:41 +0100 Subject: [PATCH] feat: let plugin frames run work they can stop in a worker --- lib/__tests__/proxy-security-headers.test.ts | 11 +++++++++++ proxy.ts | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/lib/__tests__/proxy-security-headers.test.ts b/lib/__tests__/proxy-security-headers.test.ts index 4d1ec7ef3..48201754c 100644 --- a/lib/__tests__/proxy-security-headers.test.ts +++ b/lib/__tests__/proxy-security-headers.test.ts @@ -181,6 +181,17 @@ describe('frame-ancestors of the mail UI', () => { else process.env.ALLOWED_FRAME_ANCESTORS = original; }); + // Send checks run the admin's patterns in a worker they can stop; only + // plugin frames get it, the app keeps its script-src fallback. + it('lets plugin frames, and only them, start workers from blobs', async () => { + vi.mocked(configManager.getPolicy).mockReturnValueOnce({ features: { pluginsEnabled: true } } as never); + const frame = await proxy(new NextRequest('http://localhost:3000/plugin-sandbox', { + headers: { 'sec-fetch-dest': 'iframe', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'same-origin' }, + })); + expect(frame.headers.get('content-security-policy')).toContain("worker-src 'self' blob:"); + expect((await load('/en/mail/folder/inbox')).headers.get('content-security-policy')).not.toContain('worker-src'); + }); + it('allows only the app itself by default', async () => { delete process.env.ALLOWED_FRAME_ANCESTORS; const res = await load('/en/mail/folder/inbox'); diff --git a/proxy.ts b/proxy.ts index 6660020bd..b539c4d5a 100644 --- a/proxy.ts +++ b/proxy.ts @@ -420,6 +420,10 @@ export async function proxy(request: NextRequest) { isSandboxPath ? `form-action 'none'` : `form-action ${["'self'", ...wopiOrigins].join(" ")}`, `frame-ancestors ${frameAncestors}`, `media-src ${mediaSrc}`, + // A plugin frame may run work it has to stop at a deadline (send checks' + // admin patterns) in a worker made from a blob; it gets the frame's own + // connect-src, so it reaches nothing the frame can't. + ...(isSandboxPath ? [`worker-src 'self' blob:`] : []), ].join("; "); // Skip intl middleware for routes outside the localized app tree.