Electron v20 で有効化された Renderer プロセスサンドボックス化に対応する

| 4 min read
Author: masahiro-kondo masahiro-kondoの画像

Electron v20 がリリースされました。

Release electron v20.0.0 · electron/electron

Breaking Changes として、Renderer プロセスがデフォルトでサンドボックス化されました。

Renderers are now sandboxed by default unless nodeIntegration: true or sandbox: false is specified.

Information

Electron のプロセスサンドボックス化は、Chrome の機能を利用しており、Main プロセス以外はほとんどのシステムリソースへのアクセスを制限することで悪意のあるコードが引き起こす被害を制限するためのものです。

Electron BrowserWindow の webPreferences のオプションで、nodeIntegration はデフォルトで false になっているため、これまでも Renderer プロセスでは Node モジュールを require して使うことはできませんでした。ただ、preload スクリプト[1]については、sandbox: false だったため、Node モジュールを使うことができていました。

Information

Electron アプリのアーキテクチャについては、過去記事「electron-quick-start のコミット履歴で見る Electron プログラミングモデルの変遷」に詳細を書いています。

preload スクリプトのサンドボックス化について、ドキュメントには以下のようにあります。

プロセスのサンドボックス化 | Electron

レンダラープロセスがメインプロセスと通信できるようにするため、サンドボックス化したレンダラーにアタッチされるプリロードスクリプトでは Node.js API をポリフィルしたサブセットを利用できるようになっています。 Node の require に似た require 関数のモジュールを公開してありますが、これは以下 Electron や Node の組み込みモジュールのサブセットしかインポートできません。

サンドボックスが有効になった場合、preload スクリプトでは、Electron の Renderer プロセス用のモジュールしかインポートできなくなります[2]。なので、サードパーティの Node モジュールは使えません。

筆者が開発している非公式 Scrapbox アプリでは、preload スクリプトで node-fetch を使ってデータを取得するコードを書いていました[3]

このため、v20 にアップデートすると、node-fetch がインポートされずデータが取得できなくなりました。もちろん sandbox: false を設定するとこれまで通りに動作します。しかし、このオプションもいずれ廃止される恐れがありますし、よりセキュアなコードにするために推奨される実装にする方がよいでしょう。

変更前の preload スクリプトのコードです。Renderer プロセスから呼び出すデータ取得 API を定義していました[4]

const { contextBridge, ipcRenderer } = require('electron');
const fetch = require('node-fetch');

contextBridge.exposeInMainWorld(
'api', {
fetchPageInfo: async url => {
const res = await fetch(url, { headers: { /* credentials */ } });
const data = await res.json();
return data;
},
// 省略
}
);

変更後の preload スクリプトです。node-fetch の使用をやめて、ipcRenderer.invoke を使って、Main プロセスに fetch-page-info メッセージを送り、データを取得します。

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld(
'api', {
fetchPageInfo: async url => {
const data = await ipcRenderer.invoke('fetch-page-info', url);
return data;
},
// 省略
}
);

次に Main プロセスへのコード追加です。データ取得の関数を preload スクリプトから移動しました。IPC 通信のハンドラーを追加しています。Renderer プロセスから fetch-page-info メッセージを受信したらデータ取得関数を呼び出して Renderer プロセスに結果を返します。

const fetch = require('node-fetch');

ipcMain.handle('fetch-page-info', async (e, url) => {
const data = await fetchPageInfo(url);
return data;
});

async function fetchPageInfo(url) {
const res = await fetch(url, { headers: { /* credentials */ } });
const data = await res.json();
return data;
}

これで preload スクリプト(を含む Renderer プロセス)は Main プロセスとの IPC 通信だけのコードになりました。

IPC はプロセス間通信なので、ハンドラーの追加やデータのマーシャリング等が必要[5]で、少し面倒です。しかし Main プロセス以外のプロセスはサンドボックス化するという Chrome のセキュリティ方式に Electron アプリも準拠していく必要はありますし、継続的な開発のためにも必要な措置と受け止めています。


  1. Renderer プロセスのスクリプトの前に読み込ませるスクリプトで Main プロセスとの通信に使用します。 ↩︎

  2. Clipboard や ipcRenderer など Electron の API のごく一部が対象です。 ↩︎

  3. Renderer プロセスで使用できる fetch API ではデータ取得に必要な cookie 認証が使えないためです。 ↩︎

  4. Renderer プロセスからは、window.api.fetchPageInfo で呼び出せます。 ↩︎

  5. この例では JSON で結果を返しているので元のコードからの変更はありませんでした。 ↩︎

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  2. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  3. GitHub Codespaces を使いはじめる (2022-05-18)
  4. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  5. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  6. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  7. Nuxt3入門(第3回) - ユニバーサルフェッチでデータを取得する (2022-10-06)
  8. 第1回 OpenAPI Generator を使ったコード生成 (2022-06-04)
  9. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  10. Nuxt3入門(第2回) - 簡単なNuxtアプリケーションを作成する (2022-10-02)