diff --git a/server/src/nest/plugins/plugin-frame.controller.ts b/server/src/nest/plugins/plugin-frame.controller.ts index dd92e4d0f..72c3ceb0d 100644 --- a/server/src/nest/plugins/plugin-frame.controller.ts +++ b/server/src/nest/plugins/plugin-frame.controller.ts @@ -71,7 +71,11 @@ export class PluginFrameController { const connect = ["'self'", ...outbound.map((h) => (h.includes('://') ? h : `https://${h}`))].join(' '); return [ "default-src 'none'", - "script-src 'self'", + // The frame runs at an OPAQUE origin (sandbox without allow-same-origin), + // so 'self' matches nothing — inline is the only way its own script can run. + // Safe here: the sandbox (not this script-src) is the isolation boundary, + // and the plugin author controls the frame's code either way. + "script-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob:", "font-src 'self' data:", diff --git a/server/tests/integration/plugins/registry.test.ts b/server/tests/integration/plugins/registry.test.ts index 57629315c..4d3cf1cdc 100644 --- a/server/tests/integration/plugins/registry.test.ts +++ b/server/tests/integration/plugins/registry.test.ts @@ -120,7 +120,8 @@ describe('PluginRegistryService', () => { const row = testDb.prepare("SELECT status, source_repo, source_commit FROM plugins WHERE id='flight-tracker'").get() as { status: string; source_repo: string; source_commit: string }; expect(row).toMatchObject({ status: 'inactive', source_repo: 'acme/trek-flight', source_commit: 'a'.repeat(40) }); // no staging left behind - expect(fs.existsSync(path.join(dataRoot, '.staging'))).toBe(false || fs.readdirSync(path.join(dataRoot, '.staging')).length === 0); + const staging = path.join(dataRoot, '.staging'); + expect(!fs.existsSync(staging) || fs.readdirSync(staging).length === 0).toBe(true); }); it('rejects an sha256 mismatch', async () => {