refactor: ensure IpcRenderer is not bridgable (#40330)

* refactor: ensure IpcRenderer is not bridgable

* chore: add notes to breaking-changes

* spec: fix test that bridged ipcrenderer
This commit is contained in:
Samuel Attard 2023-10-31 14:29:40 -07:00 committed by GitHub
parent 39d36e4462
commit 83892ab995
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 43 deletions

View file

@ -14,6 +14,19 @@ This document uses the following convention to categorize breaking changes:
## Planned Breaking API Changes (29.0) ## Planned Breaking API Changes (29.0)
### Behavior Changed: `ipcRenderer` can no longer be sent over the `contextBridge`
Attempting to send `ipcRenderer` as an object over the `contextBridge` will now result in
an empty object on the receiving side of the bridge. This change was made to remove / mitigate
a security footgun, you should not directly expose ipcRenderer or it's methods over the bridge.
Instead provide a safe wrapper like below:
```js
contextBridge.exposeInMainWorld('app', {
onEvent: (cb) => ipcRenderer.on('foo', (e, ...args) => cb(args))
})
```
### Removed: `renderer-process-crashed` event on `app` ### Removed: `renderer-process-crashed` event on `app`
The `renderer-process-crashed` event on `app` has been removed. The `renderer-process-crashed` event on `app` has been removed.

View file

@ -3,30 +3,30 @@ import { EventEmitter } from 'events';
const { ipc } = process._linkedBinding('electron_renderer_ipc'); const { ipc } = process._linkedBinding('electron_renderer_ipc');
const internal = false; const internal = false;
class IpcRenderer extends EventEmitter implements Electron.IpcRenderer {
const ipcRenderer = new EventEmitter() as Electron.IpcRenderer; send (channel: string, ...args: any[]) {
ipcRenderer.send = function (channel, ...args) { return ipc.send(internal, channel, args);
return ipc.send(internal, channel, args);
};
ipcRenderer.sendSync = function (channel, ...args) {
return ipc.sendSync(internal, channel, args);
};
ipcRenderer.sendToHost = function (channel, ...args) {
return ipc.sendToHost(channel, args);
};
ipcRenderer.invoke = async function (channel, ...args) {
const { error, result } = await ipc.invoke(internal, channel, args);
if (error) {
throw new Error(`Error invoking remote method '${channel}': ${error}`);
} }
return result;
};
ipcRenderer.postMessage = function (channel: string, message: any, transferables: any) { sendSync (channel: string, ...args: any[]) {
return ipc.postMessage(channel, message, transferables); return ipc.sendSync(internal, channel, args);
}; }
export default ipcRenderer; sendToHost (channel: string, ...args: any[]) {
return ipc.sendToHost(channel, args);
}
async invoke (channel: string, ...args: any[]) {
const { error, result } = await ipc.invoke(internal, channel, args);
if (error) {
throw new Error(`Error invoking remote method '${channel}': ${error}`);
}
return result;
}
postMessage (channel: string, message: any, transferables: any) {
return ipc.postMessage(channel, message, transferables);
}
}
export default new IpcRenderer();

View file

@ -4,20 +4,22 @@ const { ipc } = process._linkedBinding('electron_renderer_ipc');
const internal = true; const internal = true;
export const ipcRendererInternal = new EventEmitter() as any as ElectronInternal.IpcRendererInternal; class IpcRendererInternal extends EventEmitter implements ElectronInternal.IpcRendererInternal {
send (channel: string, ...args: any[]) {
ipcRendererInternal.send = function (channel, ...args) { return ipc.send(internal, channel, args);
return ipc.send(internal, channel, args);
};
ipcRendererInternal.sendSync = function (channel, ...args) {
return ipc.sendSync(internal, channel, args);
};
ipcRendererInternal.invoke = async function<T> (channel: string, ...args: any[]) {
const { error, result } = await ipc.invoke<T>(internal, channel, args);
if (error) {
throw new Error(`Error invoking remote method '${channel}': ${error}`);
} }
return result;
}; sendSync (channel: string, ...args: any[]) {
return ipc.sendSync(internal, channel, args);
}
async invoke<T> (channel: string, ...args: any[]) {
const { error, result } = await ipc.invoke<T>(internal, channel, args);
if (error) {
throw new Error(`Error invoking remote method '${channel}': ${error}`);
}
return result;
};
}
export const ipcRendererInternal = new IpcRendererInternal();

View file

@ -1,7 +1,8 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', { contextBridge.exposeInMainWorld('api', {
ipcRenderer, // This is not safe, do not copy this code into your app
invoke: (...args) => ipcRenderer.invoke(...args),
run: async () => { run: async () => {
const { promises: fs } = require('node:fs'); const { promises: fs } = require('node:fs');
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {

View file

@ -1,6 +1,6 @@
const { run, ipcRenderer } = window.api; const { run, invoke } = window.api;
run().then(async () => { run().then(async () => {
const count = await ipcRenderer.invoke('reload-successful'); const count = await invoke('reload-successful');
if (count < 3) location.reload(); if (count < 3) location.reload();
}).catch(console.log); }).catch(console.log);