test: service worker contextBridge leak (#45852)

* test: contextBridge prototype leak in service workers

* test: deep prototype checks
This commit is contained in:
Sam Maddock 2025-03-01 03:56:03 -05:00 committed by GitHub
parent add374ef6a
commit 21ad7cdda5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 81 additions and 1 deletions

View file

@ -388,6 +388,13 @@ describe('ServiceWorkerMain module', () => {
const result = await runTest(serviceWorker, { name: 'testEvaluate', args: ['evalConstructorName'] }); const result = await runTest(serviceWorker, { name: 'testEvaluate', args: ['evalConstructorName'] });
expect(result).to.equal('ServiceWorkerGlobalScope'); 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', () => { describe('extensions', () => {

View file

@ -18,6 +18,76 @@ const tests = {
? contextBridge.executeInMainWorld({ func, args }) ? contextBridge.executeInMainWorld({ func, args })
: contextBridge.executeInMainWorld({ func }); : contextBridge.executeInMainWorld({ func });
return result; 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 }); ipcRenderer.send(`test-result-${uuid}`, { error: false, result });
} catch (error) { } catch (error) {
console.debug(`erroring test ${name} for ${uuid}`); 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
});
} }
}); });