CVE-2020-26272 (TypeScript, published 2021-01-28T19:15Z)
Description: value: The Electron framework lets you write cross-platform desktop applications using JavaScript, HTML and CSS. In affected versions of Electron IPC messages sent from the main process to a subframe in the renderer process, through webContents.sendToFrame, event.reply or when using the remote module, can in some cases be delivered to the wrong frame. If your app uses remote, calls webContents.sendToFrame, or calls event.reply in an IPC message handler then it is impacted by this issue. This has been fixed in versions 9.4.0, 10.2.0, 11.1.0, and 12.0.0-beta.9. There are no workarounds for this issue.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-668 \u2014 Exposure of Resource to Wrong Sphere
The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.
Repository: https://github.com/electron/electron
Files changed:
docs/api/structures/ipc-main-event.md
docs/api/structures/ipc-main-invoke-event.md
docs/api/web-contents.md
lib/browser/api/web-contents.ts
lib/browser/remote/server.ts
shell/browser/api/electron_api_web_contents.cc
shell/browser/api/electron_api_web_contents.h
shell/common/gin_helper/event_emitter.cc
spec-main/api-ipc-main-spec.ts
spec-main/fixtures/snapshots/proxy-window-open.snapshot.txt
spec-main/guest-window-manager-spec.ts
typings/internal-electron.d.ts
Commit message: fix: restrict sendToFrame to same-process frames by default (#26875)



Vulnerable code:
* `frameId` Integer
WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
  } else if (typeof frameId !== 'number') {
    throw new Error('Missing required frameId argument');
  return this._sendToFrame(false /* internal */, frameId, channel, args);
WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) {
  } else if (typeof frameId !== 'number') {
    throw new Error('Missing required frameId argument');
  return this._sendToFrame(true /* internal */, frameId, channel, args);
    event.sender.sendToFrame(event.frameId, ...args);
type FinalizerInfo = { id: RendererFunctionId, webContents: electron.WebContents, frameId: number };
function setCachedRendererFunction (id: RendererFunctionId, wc: electron.WebContents, frameId: number, value: CallIntoRenderer) {
const unwrapArgs = function (sender: electron.WebContents, frameId: number, contextId: string, args: any[]) {
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
  args = unwrapArgs(event.sender, event.frameId, contextId, args);
                                        int32_t frame_id,
  auto frames = web_contents()->GetAllFrames();
  auto iter = std::find_if(frames.begin(), frames.end(), [frame_id](auto* f) {
    return f->GetRoutingID() == frame_id;
  });
  if (iter == frames.end())
    return false;
  if (!(*iter)->IsRenderFrameLive())
  (*iter)->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer);
                             int32_t frame_id,
  if (frame)
      "frameId": 1
      "frameId": 1
      "frameId": 1
      "frameId": 1
      "frameId": 1
    _sendToFrame(internal: boolean, frameId: number, channel: string, args: any): boolean;
    _sendToFrameInternal(frameId: number, channel: string, ...args: any[]): boolean;

Fixed code:
* `processId` Integer - The internal ID of the renderer process that sent this message
* `processId` Integer - The internal ID of the renderer process that sent this message
* `frameId` Integer | [number, number]
WebContents.prototype.sendToFrame = function (frame, channel, ...args) {
  } else if (!(typeof frame === 'number' || Array.isArray(frame))) {
    throw new Error('Missing required frame argument (must be number or array)');
  return this._sendToFrame(false /* internal */, frame, channel, args);
WebContents.prototype._sendToFrameInternal = function (frame, channel, ...args) {
  } else if (!(typeof frame === 'number' || Array.isArray(frame))) {
    throw new Error('Missing required frame argument (must be number or array)');
  return this._sendToFrame(true /* internal */, frame, channel, args);
  const { processId, frameId } = event;
    event.sender.sendToFrame([processId, frameId], ...args);
type FinalizerInfo = { id: RendererFunctionId, webContents: electron.WebContents, frameId: [number, number] };
function setCachedRendererFunction (id: RendererFunctionId, wc: electron.WebContents, frameId: [number, number], value: CallIntoRenderer) {
const unwrapArgs = function (sender: electron.WebContents, frameId: [number, number], contextId: string, args: any[]) {
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
  args = unwrapArgs(event.sender, [event.processId, event.frameId], contextId, args);
                                        v8::Local<v8::Value> frame,
  int32_t frame_id;
  int32_t process_id;
  if (gin::ConvertFromV8(isolate, frame, &frame_id)) {
    process_id = web_contents()->GetMainFrame()->GetProcess()->GetID();
  } else {
    std::vector<int32_t> id_pair;
    if (gin::ConvertFromV8(isolate, frame, &id_pair) && id_pair.size() == 2) {
      process_id = id_pair[0];
      frame_id = id_pair[1];
    } else {
      isolate->ThrowException(v8::Exception::Error(gin::StringToV8(
          isolate,
          "frameId must be a number or a pair of [processId, frameId]")));
      return false;
    }
  }

  auto* rfh = content::RenderFrameHost::FromID(process_id, frame_id);
  if (!rfh || !rfh->IsRenderFrameLive() ||
      content::WebContents::FromRenderFrameHost(rfh) != web_contents())
  rfh->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer);
                             v8::Local<v8::Value> frame,
#include "content/public/browser/render_process_host.h"
  if (frame) {
    dict.Set("processId", frame->GetProcess()->GetID());
  }
import { defer } from './spec-helpers';

    it('can be replied to', async () => {
      ipcMain.on('test-echo', (e, arg) => {
        e.reply('test-echo', arg);
      });
      defer(() => {
        ipcMain.removeAllListeners('test-echo');
      });

      const w = new BrowserWindow({
        show: false,
        webPreferences: {
          nodeIntegration: true
        }
      });
      w.loadURL('about:blank');
      const v = await w.webContents.executeJavaScript(`new Promise((resolve, reject) => {
        const { ipcRenderer } = require('electron')
        ipcRenderer.send('test-echo', 'hello')
        ipcRenderer.on('test-echo', (e, v) => {
          resolve(v)
        })
      })`);
      expect(v).to.equal('hello');
    });
      "frameId": 1,
      "processId": "placeholder-process-id"
      "frameId": 1,
      "processId": "placeholder-process-id"
      "frameId": 1,
      "processId": "placeholder-process-id"
      "frameId": 1,
      "processId": "placeholder-process-id"
      "frameId": 1,
      "processId": "placeholder-process-id"
    if (key === 'processId' && typeof value === 'number') {
      return 'placeholder-process-id';
    }
    _sendToFrame(internal: boolean, frameId: number | [number, number], channel: string, args: any): boolean;
    _sendToFrameInternal(frameId: number | [number, number], channel: string, ...args: any[]): boolean;