diff --git a/spec/api-service-worker-main-spec.ts b/spec/api-service-worker-main-spec.ts index 6e1edced5cc2..f42d3590125f 100644 --- a/spec/api-service-worker-main-spec.ts +++ b/spec/api-service-worker-main-spec.ts @@ -388,6 +388,13 @@ describe('ServiceWorkerMain module', () => { const result = await runTest(serviceWorker, { name: 'testEvaluate', args: ['evalConstructorName'] }); expect(result).to.equal('ServiceWorkerGlobalScope'); }); + + it('does not leak prototypes', async () => { + loadWorkerScript(); + const serviceWorker = await waitForServiceWorker('running'); + const result = await runTest(serviceWorker, { name: 'testPrototypeLeak', args: [] }); + expect(result).to.be.true(); + }); }); describe('extensions', () => { diff --git a/spec/fixtures/api/preload-realm/preload-tests.js b/spec/fixtures/api/preload-realm/preload-tests.js index bdea9737766a..e366b8f3a6d1 100644 --- a/spec/fixtures/api/preload-realm/preload-tests.js +++ b/spec/fixtures/api/preload-realm/preload-tests.js @@ -18,6 +18,76 @@ const tests = { ? contextBridge.executeInMainWorld({ func, args }) : contextBridge.executeInMainWorld({ func }); return result; + }, + testPrototypeLeak: () => { + const checkPrototypes = (value) => { + // Get prototype in preload world + const prototype = Object.getPrototypeOf(value); + const constructorName = prototype.constructor.name; + + const result = contextBridge.executeInMainWorld({ + func: (value) => { + // Deeply check that value prototypes exist in the local world + const check = (v) => { + if (typeof v === 'undefined' || v === null) return true; + const prototype = Object.getPrototypeOf(v); + const constructorName = prototype.constructor.name; + const localPrototype = globalThis[constructorName].prototype; + if (prototype !== localPrototype) return false; + if (Array.isArray(v)) return v.every(check); + if (typeof v === 'object') return Object.values(v).every(check); + if (typeof v === 'function') return check(v()); + return true; + }; + return { protoMatches: check(value), value }; + }, + args: [value, constructorName] + }); + + // Deeply check that value prototypes exist in the local world + const check = (v) => { + if (typeof v === 'undefined' || v === null) return true; + const prototype = Object.getPrototypeOf(v); + const constructorName = prototype.constructor.name; + const localPrototype = globalThis[constructorName].prototype; + if (prototype !== localPrototype) return false; + if (Array.isArray(v)) return v.every(check); + if (typeof v === 'object') return Object.values(v).every(check); + if (typeof v === 'function') return check(v()); + return true; + }; + + return ( + // Prototype matched in main world + result.protoMatches && + // Returned value matches prototype + check(result.value) + ); + }; + + const values = [ + 123, + 'string', + true, + [], + [123, 'string', true, ['foo']], + Symbol('foo'), + 10n, + {}, + Promise.resolve(), + () => {}, + () => () => null, + { [Symbol('foo')]: 123 } + ]; + + for (const value of values) { + if (!checkPrototypes(value)) { + const constructorName = Object.getPrototypeOf(value).constructor.name; + return `${constructorName} (${value}) leaked in service worker preload`; + } + } + + return true; } }; @@ -29,6 +99,9 @@ ipcRenderer.on('test', async (_event, uuid, name, ...args) => { ipcRenderer.send(`test-result-${uuid}`, { error: false, result }); } catch (error) { console.debug(`erroring test ${name} for ${uuid}`); - ipcRenderer.send(`test-result-${uuid}`, { error: true, result: error.message }); + ipcRenderer.send(`test-result-${uuid}`, { + error: true, + result: error.message + }); } });