T-CREATOR

Electron セキュリティ設定チートシート:webPreferences/CSP/許可リスト早見表

Electron セキュリティ設定チートシート:webPreferences/CSP/許可リスト早見表

Electron でアプリケーションを開発する際、最も重要なのがセキュリティ設定です。一つの設定ミスが、XSS 攻撃やリモートコード実行の脆弱性につながる可能性があります。

本記事では、Electron のセキュリティ設定の中核となる webPreferences、Content Security Policy(CSP)、そして許可リスト(allowlist)の設定方法を、すぐに使えるチートシート形式でまとめました。初心者の方でも迷わず安全な設定ができるよう、具体的なコード例と共に解説していきます。

セキュリティ設定早見表

まず、すぐに使える主要なセキュリティ設定を一覧でご紹介します。

webPreferences の推奨設定一覧

#設定項目推奨値重要度効果
1nodeIntegrationfalse★★★Renderer での Node.js API 使用を禁止
2contextIsolationtrue★★★Preload と Renderer のコンテキストを分離
3sandboxtrue★★☆Chromium サンドボックスを有効化
4webSecuritytrue★★★同一オリジンポリシーなどを有効化
5allowRunningInsecureContentfalse★★☆HTTPS 内での HTTP コンテンツ実行を禁止
6enableRemoteModulefalse★★★remote モジュールを無効化
7nodeIntegrationInWorkerfalse★★★Worker での Node.js API 使用を禁止
8nodeIntegrationInSubFramesfalse★★★iframe での Node.js API 使用を禁止
9preloadパス指定-安全な API を公開する Preload スクリプト

CSP ディレクティブ推奨設定一覧

#ディレクティブ推奨値用途
1default-src'self'すべてのリソースのデフォルト
2script-src'self'JavaScript の読み込み元制限
3style-src'self' 'unsafe-inline'CSS の読み込み元制限
4img-src'self' data: https:画像の読み込み元制限
5connect-src'self' + API ドメインXHR/WebSocket の通信先制限
6object-src'none'プラグイン実行を禁止
7base-uri'self'base URL を制限
8form-action'none' または 'self'フォーム送信先を制限

必須セキュリティチェックリスト

#チェック項目確認方法
1nodeIntegrationfalse になっているかBrowserWindow の webPreferences を確認
2contextIsolationtrue になっているかBrowserWindow の webPreferences を確認
3CSP が設定されているかHTML の meta タグまたはヘッダーを確認
4Preload スクリプトで contextBridge を使用しているかpreload.ts/js ファイルを確認
5ナビゲーションガードが設定されているかwill-navigate イベントハンドラを確認
6外部リンクをブラウザで開く設定になっているかsetWindowOpenHandler の実装を確認
7DevTools で requireundefined になっているかコンソールで typeof require を実行
8CSP 違反エラーが出ていないかDevTools のコンソールを確認

背景

Electron のセキュリティリスク

Electron は、Chromium と Node.js を統合したフレームワークです。この統合により、Web 技術でデスクトップアプリを開発できる反面、Web とネイティブ環境の両方のセキュリティリスクを抱えることになります。

mermaidflowchart TB
  app["Electron アプリ"] --> renderer["Renderer プロセス<br/>(Chromium)"]
  app --> main["Main プロセス<br/>(Node.js)"]

  renderer -->|"デフォルト設定では<br/>危険"| node["Node.js API<br/>アクセス可能"]
  renderer -->|"外部コンテンツ"| xss["XSS 攻撃リスク"]

  node --> rce["リモートコード<br/>実行の危険"]
  xss --> rce

  style rce fill:#ff6b6b
  style xss fill:#ffd93d
  style node fill:#ffd93d

上の図が示すように、Renderer プロセスが Node.js API に直接アクセスできる状態では、XSS 攻撃を受けた際にシステム全体が危険にさらされます。

Electron アプリが抱える主なセキュリティリスクは以下の通りです:

  • Node.js API への不正アクセス:Renderer プロセスから直接ファイルシステムやプロセスを操作できてしまう
  • 外部コンテンツの読み込み:信頼できないリモートコンテンツを読み込むことで XSS 攻撃を受けるリスク
  • プロトコル制限の欠如file:​/​​/​data: プロトコルの無制限な利用による情報漏洩

セキュリティ設定の 3 本柱

これらのリスクを軽減するため、Electron には以下の 3 つの主要なセキュリティメカニズムが用意されています。

#メカニズム役割設定場所
1webPreferencesプロセス間の権限分離を制御BrowserWindow 生成時
2CSPコンテンツ読み込み元を制限HTML の meta タグまたはヘッダー
3許可リスト特定のリソースへのアクセスを明示的に許可webRequest API や設定ファイル

これら 3 つを適切に組み合わせることで、多層防御によるセキュアな Electron アプリを実現できます。

課題

セキュリティ設定の複雑さ

Electron のセキュリティ設定には、いくつかの課題があります。

設定項目が多岐にわたる

webPreferences だけでも 20 以上の設定項目があり、それぞれが相互に影響し合います。どの設定をどのように組み合わせるべきか、初心者には判断が難しいでしょう。

デフォルト設定の危険性

古いバージョンの Electron では、デフォルトで nodeIntegration: true になっていました。この設定では、Renderer プロセスから直接 Node.js API にアクセスできてしまうため、極めて危険です。

バージョンによる違い

Electron のバージョンアップに伴い、デフォルト設定や推奨設定が変更されることがあります。古いドキュメントを参照していると、現在のベストプラクティスから外れた設定をしてしまう可能性があります。

よくある設定ミス

実際の開発現場でよく見られる設定ミスを見ていきましょう。

mermaidflowchart LR
  mistake1["nodeIntegration: true"] --> risk1["Node.js API に<br/>直接アクセス可能"]
  mistake2["contextIsolation: false"] --> risk2["グローバル汚染<br/>リスク"]
  mistake3["CSP なし"] --> risk3["外部スクリプト<br/>実行可能"]
  mistake4["allowRunningInsecure<br/>Content: true"] --> risk4["HTTPS 強制なし"]

  risk1 --> danger["セキュリティ<br/>脆弱性"]
  risk2 --> danger
  risk3 --> danger
  risk4 --> danger

  style danger fill:#ff6b6b
  style mistake1 fill:#ffd93d
  style mistake2 fill:#ffd93d
  style mistake3 fill:#ffd93d
  style mistake4 fill:#ffd93d

これらの設定ミスは、それぞれ単独でも危険ですが、組み合わさることで攻撃の入口を大きく広げてしまいます。

#設定ミス危険性攻撃例
1nodeIntegration: true★★★XSS から OS コマンド実行
2contextIsolation: false★★★プロトタイプ汚染攻撃
3CSP の未設定★★☆外部スクリプトインジェクション
4webSecurity: false★★★CORS 制限のバイパス
5allowRunningInsecureContent: true★☆☆中間者攻撃

これらの課題を解決するには、推奨設定を体系的に理解し、チートシートとして手元に置いておくことが効果的です。

解決策

webPreferences の推奨設定

まず、webPreferences の推奨設定を見ていきましょう。これは BrowserWindow を生成する際に指定します。

基本設定:セキュアな BrowserWindow の作成

BrowserWindow を作成する際の基本的なセキュリティ設定です。

typescriptimport { BrowserWindow } from 'electron';
typescript// セキュアな BrowserWindow の作成
const createSecureWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      // 以下でセキュリティ設定を指定
    },
  });

  return win;
};

次に、webPreferences の中身を段階的に設定していきます。

Node.js 統合の無効化

Renderer プロセスから Node.js API への直接アクセスを無効化します。

typescriptwebPreferences: {
  // Node.js 統合を無効化(最重要)
  nodeIntegration: false,

  // Worker スレッドでの Node.js 統合も無効化
  nodeIntegrationInWorker: false,

  // サブフレーム(iframe など)でも Node.js 統合を無効化
  nodeIntegrationInSubFrames: false,
}

nodeIntegrationfalse に設定することで、Renderer プロセスから require()process などの Node.js API を直接使えなくなります。これにより、XSS 攻撃を受けてもシステムへの直接アクセスを防げます。

コンテキスト分離の有効化

Renderer プロセスと Preload スクリプトのコンテキストを分離します。

typescriptwebPreferences: {
  // コンテキスト分離を有効化(最重要)
  contextIsolation: true,

  // Preload スクリプトの指定
  preload: path.join(__dirname, 'preload.js'),
}

contextIsolation: true にすることで、Preload スクリプトと Renderer プロセスが異なる JavaScript コンテキストで実行されます。これにより、グローバルスコープの汚染やプロトタイプチェーンの改ざんを防げるのです。

Web セキュリティの有効化

同一オリジンポリシーなどの Web セキュリティ機能を有効化します。

typescriptwebPreferences: {
  // Web セキュリティを有効化
  webSecurity: true,

  // HTTPS ページ内での HTTP コンテンツ実行を禁止
  allowRunningInsecureContent: false,
}

webSecurity: false にすると、CORS(Cross-Origin Resource Sharing)制限がバイパスされ、任意のドメインからリソースを取得できてしまいます。開発時の便宜のために無効化する例を見かけますが、本番環境では必ず true に設定してください。

リモートモジュールの無効化

古い Electron の remote モジュールを無効化します。

typescriptwebPreferences: {
  // remote モジュールを無効化
  enableRemoteModule: false,
}

Electron 14 以降では @electron​/​remote として別パッケージになっていますが、古いバージョンを使用している場合は明示的に無効化しましょう。remote モジュールは Renderer から Main プロセスのオブジェクトに直接アクセスできるため、セキュリティリスクが高いです。

サンドボックスの有効化

Chromium のサンドボックス機能を有効化します。

typescriptwebPreferences: {
  // サンドボックスを有効化
  sandbox: true,
}

sandbox: true にすると、Renderer プロセスが Chromium のサンドボックス内で実行されます。これにより、Renderer プロセスが侵害されても、システムへの影響を最小限に抑えられます。

ただし、サンドボックスを有効化すると、Preload スクリプトでも Node.js API の一部が使用できなくなります。この場合、contextBridge を使った安全な API 公開が必要です。

完全な webPreferences 設定例

これまでの設定をまとめた、完全なセキュア設定の例です。

typescriptimport { app, BrowserWindow } from 'electron';
import * as path from 'path';
typescriptfunction createSecureWindow() {
  const mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      // Node.js 統合関連(すべて無効化)
      nodeIntegration: false,
      nodeIntegrationInWorker: false,
      nodeIntegrationInSubFrames: false,

      // コンテキスト分離(有効化)
      contextIsolation: true,

      // Web セキュリティ(有効化)
      webSecurity: true,
      allowRunningInsecureContent: false,

      // リモートモジュール(無効化)
      enableRemoteModule: false,

      // サンドボックス(有効化)
      sandbox: true,

      // Preload スクリプト
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  return mainWindow;
}
typescriptapp.whenReady().then(() => {
  const win = createSecureWindow();

  // セキュアなコンテンツの読み込み
  win.loadFile('index.html');
});

この設定が、2024 年時点での Electron セキュリティのベストプラクティスとなります。

webPreferences 設定早見表

すべての主要な webPreferences 設定を一覧表にまとめました。

#設定項目推奨値説明危険度(false 時)
1nodeIntegrationfalseRenderer での Node.js API 使用を禁止★★★
2nodeIntegrationInWorkerfalseWorker での Node.js API 使用を禁止★★★
3nodeIntegrationInSubFramesfalseiframe での Node.js API 使用を禁止★★★
4contextIsolationtruePreload と Renderer のコンテキストを分離★★★
5webSecuritytrue同一オリジンポリシーなどを有効化★★★
6allowRunningInsecureContentfalseHTTPS 内での HTTP コンテンツ実行を禁止★★☆
7enableRemoteModulefalseremote モジュールを無効化★★★
8sandboxtrueChromium サンドボックスを有効化★★☆
9preloadパス指定安全な API を公開する Preload スクリプト-

CSP(Content Security Policy)の設定

CSP は、読み込み可能なリソースの出元を制限するセキュリティ機構です。

CSP の基本概念

CSP を設定することで、以下のような攻撃を防ぐことができます:

  • インラインスクリプトの実行防止
  • 外部ドメインからのスクリプト読み込み制限
  • eval() などの危険な関数の使用禁止
mermaidflowchart TB
  page["HTML ページ"] --> csp["CSP ヘッダー"]

  csp --> allow["許可リスト"]
  csp --> block["ブロックリスト"]

  allow -->|"self のみ"| script1["同一オリジン<br/>スクリプト OK"]
  allow -->|"特定ドメイン"| script2["cdn.example.com<br/>OK"]

  block -->|"それ以外"| script3["外部スクリプト<br/>ブロック"]
  block -->|"inline"| script4["インラインスクリプト<br/>ブロック"]

  style script3 fill:#ff6b6b
  style script4 fill:#ff6b6b
  style script1 fill:#95e1d3
  style script2 fill:#95e1d3

上の図のように、CSP は許可するリソースを明示的に指定し、それ以外をすべてブロックする「ホワイトリスト方式」で動作します。

HTML での CSP 設定

HTML ファイルの <head> 内に <meta> タグで CSP を設定できます。

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />

    <!-- CSP の設定 -->
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'"
    />

    <title>セキュアな Electron アプリ</title>
  </head>
</html>

この設定では、同一オリジン('self')からのリソースのみを許可しています。

厳格な CSP 設定例

最も厳格な CSP 設定の例です。

html<meta
  http-equiv="Content-Security-Policy"
  content="
        default-src 'none';
        script-src 'self';
        style-src 'self' 'unsafe-inline';
        img-src 'self' data:;
        font-src 'self';
        connect-src 'self';
        base-uri 'self';
        form-action 'none';
      "
/>

各ディレクティブの意味は以下の通りです:

  • default-src 'none':デフォルトですべてのリソース読み込みを禁止
  • script-src 'self':スクリプトは同一オリジンのみ許可
  • style-src 'self' 'unsafe-inline':CSS は同一オリジンとインライン CSS を許可
  • img-src 'self' data::画像は同一オリジンと data URI を許可
  • font-src 'self':フォントは同一オリジンのみ許可
  • connect-src 'self':XHR/WebSocket は同一オリジンのみ許可
  • base-uri 'self'<base> タグは同一オリジンのみ許可
  • form-action 'none':フォーム送信を禁止

外部 API を使う場合の CSP

外部 API(例:GitHub API)を使用する場合の設定例です。

html<meta
  http-equiv="Content-Security-Policy"
  content="
        default-src 'self';
        script-src 'self';
        style-src 'self' 'unsafe-inline';
        img-src 'self' data: https:;
        connect-src 'self' https://api.github.com;
      "
/>

connect-srchttps:​/​​/​api.github.com を追加することで、GitHub API への接続を許可しています。

JavaScript での CSP 設定

Main プロセスから HTTP ヘッダーとして CSP を設定することもできます。

typescriptimport { app, BrowserWindow, session } from 'electron';
typescriptapp.whenReady().then(() => {
  // CSP をヘッダーとして設定
  session.defaultSession.webRequest.onHeadersReceived(
    (details, callback) => {
      callback({
        responseHeaders: {
          ...details.responseHeaders,
          'Content-Security-Policy': [
            "default-src 'self'; script-src 'self'",
          ],
        },
      });
    }
  );

  const win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
    },
  });

  win.loadFile('index.html');
});

この方法では、すべてのレスポンスに CSP ヘッダーを追加できます。HTML ファイルを変更せずに CSP を適用したい場合に便利です。

CSP ディレクティブ早見表

主要な CSP ディレクティブを一覧表にまとめました。

#ディレクティブ用途推奨値説明
1default-srcすべてのリソースのデフォルト'self' または 'none'他で指定されていないリソースに適用
2script-srcJavaScript'self'スクリプトの読み込み元を制限
3style-srcCSS'self' 'unsafe-inline'スタイルシートの読み込み元を制限
4img-src画像'self' data: https:画像の読み込み元を制限
5font-srcフォント'self'フォントの読み込み元を制限
6connect-srcXHR/WebSocket'self' + API ドメイン通信先を制限
7media-src音声・動画'self'メディアの読み込み元を制限
8object-srcプラグイン'none'<object> などを禁止
9frame-srciframe'none' または 'self'iframe の読み込み元を制限
10base-uri<base> タグ'self'base URL を制限
11form-actionフォーム送信先'none' または 'self'フォーム送信先を制限

許可リスト(Allowlist)の設定

特定のリソースへのアクセスを明示的に許可する仕組みです。

プロトコル許可リスト

Electron アプリで使用するカスタムプロトコルや外部プロトコルを制御します。

typescriptimport { app, protocol } from 'electron';
typescriptapp.whenReady().then(() => {
  // カスタムプロトコルの登録
  protocol.registerFileProtocol(
    'app',
    (request, callback) => {
      const url = request.url.substring(6); // 'app://' を除去
      callback({ path: path.join(__dirname, url) });
    }
  );

  // プロトコルの許可リストに追加
  protocol.registerSchemesAsPrivileged([
    {
      scheme: 'app',
      privileges: {
        standard: true, // 標準プロトコルとして扱う
        secure: true, // セキュアなプロトコルとして扱う
        supportFetchAPI: true, // Fetch API をサポート
        corsEnabled: true, // CORS を有効化
      },
    },
  ]);
});

この設定により、app:​/​​/​ プロトコルを安全に使用できるようになります。

ナビゲーション許可リスト

Renderer プロセスが遷移できる URL を制限します。

typescriptimport { app, BrowserWindow } from 'electron';
typescript// 許可する URL のリスト
const ALLOWED_URLS = [
  'https://example.com',
  'https://api.example.com',
];
typescriptfunction createWindowWithNavGuard() {
  const win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
    },
  });

  // ナビゲーションイベントの監視
  win.webContents.on('will-navigate', (event, url) => {
    const parsedUrl = new URL(url);

    // 許可リストにないドメインへのナビゲーションをブロック
    const isAllowed = ALLOWED_URLS.some((allowedUrl) => {
      return url.startsWith(allowedUrl);
    });

    if (!isAllowed) {
      console.warn(`ブロックされたナビゲーション: ${url}`);
      event.preventDefault();
    }
  });

  return win;
}

will-navigate イベントをフックすることで、不正な URL への遷移を防ぎます。

新しいウィンドウの許可リスト

window.open()<a target="_blank"> による新しいウィンドウの作成を制御します。

typescriptfunction createWindowWithPopupGuard() {
  const win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
    },
  });

  // 新しいウィンドウ作成の監視
  win.webContents.setWindowOpenHandler(({ url }) => {
    const parsedUrl = new URL(url);

    // 許可リストのチェック
    const isAllowed = ALLOWED_URLS.some((allowedUrl) => {
      return url.startsWith(allowedUrl);
    });

    if (!isAllowed) {
      console.warn(`ブロックされたウィンドウ: ${url}`);
      return { action: 'deny' };
    }

    // 許可された URL はデフォルトブラウザで開く
    return {
      action: 'allow',
      overrideBrowserWindowOptions: {
        webPreferences: {
          nodeIntegration: false,
          contextIsolation: true,
        },
      },
    };
  });

  return win;
}

setWindowOpenHandler を使うことで、新しいウィンドウの作成を細かく制御できます。

外部リンクの制御

外部リンクをシステムのデフォルトブラウザで開く設定です。

typescriptimport { shell } from 'electron';
typescriptfunction createWindowWithExternalLinks() {
  const win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
    },
  });

  // 外部リンクをデフォルトブラウザで開く
  win.webContents.setWindowOpenHandler(({ url }) => {
    // 外部 URL はシステムブラウザで開く
    if (
      url.startsWith('http://') ||
      url.startsWith('https://')
    ) {
      shell.openExternal(url);
      return { action: 'deny' };
    }

    return { action: 'allow' };
  });

  return win;
}

この設定により、Electron アプリ内で外部サイトを開かず、ユーザーのデフォルトブラウザで開くようになります。

具体例

実践的なセキュア Electron アプリの構成

実際のアプリケーションで使える、完全なセキュリティ設定の実装例を見ていきましょう。

プロジェクト構成

perlmy-electron-app/
├── src/
│   ├── main/
│   │   ├── main.ts          # Main プロセス
│   │   └── security.ts      # セキュリティ設定
│   ├── preload/
│   │   └── preload.ts       # Preload スクリプト
│   └── renderer/
│       ├── index.html       # HTML
│       └── renderer.ts      # Renderer スクリプト
└── package.json

この構成で、各ファイルの役割を明確に分離します。

Main プロセス(main.ts)

Main プロセスでセキュアな BrowserWindow を作成します。

typescript// src/main/main.ts
import { app, BrowserWindow, ipcMain } from 'electron';
import * as path from 'path';
import { setupSecurity } from './security';
typescript// グローバル参照を保持
let mainWindow: BrowserWindow | null = null;
typescriptfunction createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      // Node.js 統合を無効化
      nodeIntegration: false,
      nodeIntegrationInWorker: false,
      nodeIntegrationInSubFrames: false,

      // コンテキスト分離を有効化
      contextIsolation: true,

      // セキュリティ機能を有効化
      webSecurity: true,
      allowRunningInsecureContent: false,

      // リモートモジュールを無効化
      enableRemoteModule: false,

      // サンドボックスを有効化
      sandbox: true,

      // Preload スクリプト
      preload: path.join(
        __dirname,
        '../preload/preload.js'
      ),
    },
  });

  // HTML ファイルの読み込み
  mainWindow.loadFile(
    path.join(__dirname, '../renderer/index.html')
  );

  // ウィンドウが閉じられたときの処理
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}
typescript// アプリの起動
app.whenReady().then(() => {
  // セキュリティ設定を適用
  setupSecurity();

  // ウィンドウを作成
  createWindow();

  // macOS での再アクティブ化
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});
typescript// すべてのウィンドウが閉じられたときの処理
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

セキュリティ設定(security.ts)

セキュリティ関連の設定を一箇所にまとめます。

typescript// src/main/security.ts
import { app, session, shell } from 'electron';
typescript// 許可する URL のリスト
const ALLOWED_ORIGINS = [
  'https://api.github.com',
  'https://api.example.com',
];
typescriptexport function setupSecurity() {
  // CSP の設定
  setupCSP();

  // ナビゲーションガード
  setupNavigationGuard();

  // 新しいウィンドウの制御
  setupWindowOpenHandler();

  // プロトコルの設定
  setupProtocols();
}
typescript// CSP をヘッダーとして設定
function setupCSP() {
  session.defaultSession.webRequest.onHeadersReceived(
    (details, callback) => {
      callback({
        responseHeaders: {
          ...details.responseHeaders,
          'Content-Security-Policy': [
            [
              "default-src 'self'",
              "script-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "img-src 'self' data: https:",
              "font-src 'self'",
              `connect-src 'self' ${ALLOWED_ORIGINS.join(
                ' '
              )}`,
              "object-src 'none'",
              "base-uri 'self'",
              "form-action 'none'",
            ].join('; '),
          ],
        },
      });
    }
  );
}
typescript// ナビゲーションガードの設定
function setupNavigationGuard() {
  app.on('web-contents-created', (event, contents) => {
    contents.on('will-navigate', (navEvent, url) => {
      const parsedUrl = new URL(url);

      // file:// プロトコルのみ許可
      if (parsedUrl.protocol !== 'file:') {
        console.warn(
          `ブロックされたナビゲーション: ${url}`
        );
        navEvent.preventDefault();
      }
    });
  });
}
typescript// 新しいウィンドウの制御
function setupWindowOpenHandler() {
  app.on('web-contents-created', (event, contents) => {
    contents.setWindowOpenHandler(({ url }) => {
      // 外部 URL はデフォルトブラウザで開く
      if (
        url.startsWith('http://') ||
        url.startsWith('https://')
      ) {
        shell.openExternal(url);
        return { action: 'deny' };
      }

      return { action: 'deny' };
    });
  });
}
typescript// カスタムプロトコルの設定
function setupProtocols() {
  // app:// プロトコルの特権設定
  // これは app.whenReady() の前に呼ぶ必要がある
}

Preload スクリプト(preload.ts)

contextBridge を使って安全に API を公開します。

typescript// src/preload/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
typescript// Renderer に公開する API
contextBridge.exposeInMainWorld('electronAPI', {
  // Main プロセスへメッセージ送信
  sendMessage: (channel: string, data: any) => {
    // 許可されたチャンネルのみ送信可能
    const validChannels = ['toMain'];
    if (validChannels.includes(channel)) {
      ipcRenderer.send(channel, data);
    }
  },

  // Main プロセスからメッセージ受信
  onMessage: (channel: string, callback: Function) => {
    // 許可されたチャンネルのみ受信可能
    const validChannels = ['fromMain'];
    if (validChannels.includes(channel)) {
      ipcRenderer.on(channel, (event, ...args) =>
        callback(...args)
      );
    }
  },
});

この設定により、Renderer プロセスは window.electronAPI を通じてのみ Main プロセスと通信できます。

HTML ファイル(index.html)

CSP を含む HTML ファイルの例です。

html<!-- src/renderer/index.html -->
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />

    <!-- CSP の設定(二重の保護) -->
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
    />

    <title>セキュアな Electron アプリ</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
</html>
html<body>
  <div id="app">
    <h1>セキュアな Electron アプリケーション</h1>
    <button id="sendBtn">メッセージ送信</button>
    <div id="output"></div>
  </div>

  <script src="renderer.js"></script>
</body>
</html>

Renderer スクリプト(renderer.ts)

Renderer プロセスで動作するスクリプトです。

typescript// src/renderer/renderer.ts

// TypeScript の型定義
declare global {
  interface Window {
    electronAPI: {
      sendMessage: (channel: string, data: any) => void;
      onMessage: (
        channel: string,
        callback: Function
      ) => void;
    };
  }
}
typescript// DOM の初期化
document.addEventListener('DOMContentLoaded', () => {
  const sendBtn = document.getElementById('sendBtn');
  const output = document.getElementById('output');

  // ボタンクリック時の処理
  sendBtn?.addEventListener('click', () => {
    // Preload で公開した API を使用
    window.electronAPI.sendMessage('toMain', {
      message: 'Hello from Renderer',
    });
  });

  // Main からのメッセージを受信
  window.electronAPI.onMessage('fromMain', (data: any) => {
    if (output) {
      output.textContent = `受信: ${JSON.stringify(data)}`;
    }
  });
});

この構成により、Renderer プロセスは直接 Node.js API にアクセスできず、contextBridge で公開された API のみを使用します。

セキュリティ検証の実施

実装したセキュリティ設定が正しく動作しているか検証しましょう。

DevTools でのテスト

開発者ツールのコンソールで以下を実行してみてください。

javascript// Node.js API へのアクセステスト(失敗するはず)
typeof require; // => 'undefined'
typeof process; // => 'undefined'
typeof __dirname; // => 'undefined'
javascript// contextBridge で公開した API のテスト(成功するはず)
typeof window.electronAPI; // => 'object'
window.electronAPI.sendMessage('toMain', { test: true }); // => 動作する

すべてのテストで期待通りの結果が得られれば、セキュリティ設定は正しく機能しています。

CSP 違反の確認

DevTools のコンソールに CSP 違反のエラーが表示されないか確認します。

javascript// インラインスクリプトのテスト(ブロックされるはず)
eval('console.log("test")');
// => Refused to evaluate a string as JavaScript because 'unsafe-eval'...

CSP が正しく設定されていれば、eval() などの危険な関数は実行できません。

まとめ

Electron アプリケーションのセキュリティは、webPreferences、CSP、許可リストの 3 つの柱で構成されます。

webPreferences の重要設定

  • nodeIntegration: false で Node.js API への直接アクセスを禁止
  • contextIsolation: true でコンテキストを分離
  • sandbox: true で Chromium サンドボックスを有効化

CSP の活用

  • default-src 'self' ですべてのリソースを同一オリジンに制限
  • 必要な外部リソースのみ明示的に許可
  • HTML とヘッダーの両方で設定し二重に保護

許可リストによる制御

  • ナビゲーション先を制限し、不正な URL への遷移を防止
  • 新しいウィンドウの作成を制御
  • 外部リンクはシステムブラウザで開く

これらの設定を組み合わせることで、XSS 攻撃やリモートコード実行などの脅威から Electron アプリを守ることができます。本記事で紹介したチートシートを参考に、ぜひセキュアなアプリケーション開発を実践してください。

セキュリティは一度設定すれば終わりではなく、Electron のバージョンアップや新たな脅威に応じて継続的に見直す必要があります。定期的に公式ドキュメントを確認し、最新のベストプラクティスを取り入れていきましょう。

関連リンク