Browse Source

test: service worker contextBridge leak (#45852)

* test: contextBridge prototype leak in service workers

* test: deep prototype checks
Sam Maddock 1 month ago
parent
commit
21ad7cdda5

+ 7 - 0
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', () => {

+ 74 - 1
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
+    });
   }
 });