建設予定地

当面はやったことの備忘録

ElectronでセキュアなIPC通信を実装する

はじめに

こんにちは!0yu(@denham95173179)です。
この記事はLOCAL Students Advent Calendar 2021への寄稿です。
adventar.org

先日、 SC4Y ('21#4) IT・情報系 北海道まったりLT大会に登壇させていただき、最近細々とElectronで作っているマークダウンエディタについてお話してきました。
sc4y.connpass.com

GitHubはこちら
github.com

今回の実装でやりたかったことは以下のようなことでした。

  • レンダラープロセスからのファイル操作を実装する
  • Electronでファイル操作を扱うdialogモジュールをレンダラプロセス側からIPC通信で呼び出す
  • Node.jsによるファイルの読み書きをレンダラープロセス側で行なう

Electron13まではremoteモジュールを採用したプロセス間通信が可能でしたが、remoteモジュール*1に依存しないセキュアなIPC通信を実装したいというのが本記事の趣旨です。

何が問題なのか

remoteモジュールの削除は、破壊的変更としてElectron14から完全に削除されてしまいました。 www.electronjs.org

remoteモジュールに依存せず、レンダラープロセス側でCommonJSを記述できるようにするためには、nodeIntegrationtrueにする必要があります。
しかしながら、レンダラープロセス側からNode.jsを扱うことは本来非推奨とされてします。

// background.ts
const mainWindow = new BrowserWindow({
  webPreferences: {
    nodeIntegration: true,
    nodeIntegrationInWorker: true
  }
})
  
mainWindow.loadURL('https://example.com')

Electronでは、 セキュリティ推奨事項​のチェックリストが公式ドキュメントに記載されており、チェックリスト中の2) Do not enable Node.js Integration for Remote Contentにこれが当たります。

This recommendation is the default behavior in Electron since 5.0.0.

It is paramount that you do not enable Node.js integration in any renderer (BrowserWindow, BrowserView, or ) that loads remote content. The goal is to limit the powers you grant to remote content, thus making it dramatically more difficult for an attacker to harm your users should they gain the ability to execute JavaScript on your website.

After this, you can grant additional permissions for specific hosts. For example, if you are opening a BrowserWindow pointed at > https://example.com/, you can give that website exactly the abilities it needs, but no more.

これはなぜかというと、ブラウザ側(レンダラープロセス)でNode.jsのAPIを扱うメインプロセス側の処理を行なえるということは、例えばXSS攻撃を受けたとき、ユーザーのローカルファイルに無制限にアクセスされてしまうリスクがあるためです。
zenn.dev

このようなリスクを未然に防ぐために、安全なプロセス間通信を行うことが推奨されています。
すなわちremoteモジュールの削除された現行バージョンのElectronで、nodeIntegrationを無効にしたIPC通信を行う必要が生じます。

じゃあどうすればいいの?

ここで行なうべきことは大きく以下のようになります。

  • background.tsでnodeIntegrationを無効にする(レンダラープロセス側からのNode.jsモジュールの操作を無効にする)
  • background.tsでcontextBridgeを有効にする(コンテキストの分離*2

※ Electron16 + Vue3 + TypeScriptを想定

background.tsのnodeIntegrationを無効にします。

// background.ts
const mainWindow = new BrowserWindow({
  webPreferences: {
       + nodeIntegration: false,
       + preload: path.join(__dirname, 'preload.js')
  }
})

mainWindow.loadURL('https://example.com')

またここで、ビルド段階でpreloadのパスを読ませるため、vue.config.jsに下記のように記述します。

// vue.config.js
module.exports = {
  pluginOptions: {
    electronBuilder: {
       + preload: 'src/preload.js',
    }
  }
}

srcディレクトリ直下にpreload.tsを作成し、以下のように記述します。
これによってpreload.tsは、グローバルなwindowオブジェクトをレンダラーと共有できるようになります。

// preload.ts
const { contextBridge } = require('electron')
  
contextBridge.exposeInMainWorld('electronAPI', {
  loadPreferences: () => ipcRenderer.invoke('load-prefs')
})
// Main.vue
window.electronAPI.loadPreferences()
// ここでwindow.electronAPI.hogehoge()などする

次にrenderer.d.tsを作成し、以下のように記述することで、Windowインターフェースの拡張を行うことができます。

export interface IElectronAPI {
  loadPreferences: () => Promise<void>,
}

declare global {
  interface Window {
    electronAPI: IElectronAPI
  }
}

以上のようにすることで、レンダラープロセス側でスクリプトを書く際に、グローバルなwindowオブジェクトにアクセスできるようになります。

フォルダ構造は下記のようになっています。

├── dist_electron/
│ ├── bundled/..  # where webpack outputs compiled files
│ ├── [target platform]-unpacked/..  # unpacked Electron app (main app and supporting files)
│ ├── [application name] setup [version].[target binary (exe|dmg|rpm...)]  # installer for Electron app
│ ├── index.js  # compiled background file used for electron:serve
│ └── ...
├── public/  # Files placed here will be available through __static or process.env.BASE_URL
├── src/
│ ├── background.[js|ts]  # electron entry file (for Electron's main process)
│ ├── [main|index].[js|ts]  # your app's entry file (for Electron's render process)
│ └── ...
├── package.json  # your app's package.json file
├── ...

おわりに

ファイル操作の実装の詳細や、contextBridgeの中身について深堀った記事を(余力があれば)かきたいです。

参考文献

www.electronjs.org github.com zenn.dev

*1:レンダラプロセスからメインプロセスが持つ機能を扱うためのモジュール.12.0で非推奨となり、14.0で完全に削除された

*2:コンテキストの分離はElectron12からデフォルト値で有効となっており、現行で推奨されているセキュリティ設定です