侵入を防ごう: サンドボックスでアプリを強化
CVE-2023-4863: WebP のヒープバッファオーバーフロー が公開されたことで、この 1 週間以上は webp
を描画するソフトウェアの新規リリースの連続でした。macOS、iOS、Chrome、Firefox、そして多くの Linux ディストリビューションすべてが更新を受け取ったことでしょう。 これは Citizen Lab の調査に伴うもので、「ワシントン DC を拠点とする市民団体」の使用する iPhone が iMessage 内でゼロクリック攻撃を受けたことを発見したものです。
Electron も同日、新バージョンをリリースしました。もしあなたのアプリがユーザー提供のコンテンツを描画する場合、Electron のバージョンを更新すべきです。Electron の v27.0.0-beta.2、v26.2.1、v25.8.1、v24.8.3、v22.3.24 すべてに、webp
画像のレンダリングを行うライ ブラリ libwebp
の修正版を含んでいます。
「画像をレンダリングする」という無邪気な操作が、潜在的に危険な行為であることを私たち全員が再認識した今、私たちはこの機会を利用して、Electron にさらなる大きな攻撃が来た時、付属のプロセスサンドボックスでそれが何であろうとその爆発半径を制限できることを皆さんにお知らせしたいと思います。
サンドボックスは Electron v1 以来ずっと利用可能で、v20 ではデフォルトで有効になっています。しかし、多くのアプリ (特に以前からあるアプリ) はコードのどこかで sandbox: false
としていたり、nodeIntegration: true
になっていることがあります。これらは明示的に sandbox
を設定しない限り、等しくサンドボックスを無効化します。 そのような行いも理解できます。ご寵愛頂いてきた方であれば、おそらく require("child_process")
や require("fs")
を HTML/CSS と同じコードに投げ込んで実行する強力さを味わってきたことでしょう。
どうやって サンドボックスに移行するのかについて話す前に、まずは なぜ 移行したいのかについてご説明します。
サンドボックスは、すべてのレンダラープロセスを硬い檻で囲い、内部で何が起ころうと、コードが制限された環境内で実行されることを保証します。 コンセプトとしては Chromium よりもずっと古く、すべての主要なオペレーティングシステムで一つの機能として提供されています。 Electron と Chromium のサンドボックスは、これらのシステム機能の上に構築されています。 ユーザー生成コンテンツを表示しない場合でも、レンダ ラーが侵害される可能性を考慮すべきです。サプライチェーン攻撃のような巧妙なシナリオから、ちょっとしたバグのような単純なシナリオまで、レンダラーはあなたが意図しない動作をする可能性があります。
そうなったプロセスは CPU サイクルとメモリを自由に使うことができます。サンドボックスはこういった恐ろしい筋書きをかなり軽減してくれます。 プロセスはディスクに書き込んだり、自分自身のウィンドウを表示したりできません。 例の libwep
バグの場合、サンドボックスは攻撃者がマルウェアをインストールしたり実行したりできないようにします。 実際、従業員の iPhone を狙ったオリジナルの Pegasus 攻撃の場合、通常だとサンドボックス化されている iMessage の境界を突破して携帯電話へのアクセスを得るために、サンドボックス化されていない画像処理を特に狙っていました。 この例のような CVE が発表された場合、Electron アプリを安全なバージョンにアップグレードする必要があります。しかし、その間に攻撃者が起こせる被害は劇的に制限されます。
Electron アプリケーションを sandbox: false
から sandbox: true
に移行するのは大変な作業です。 というのも、Electron セキュリティガイドライン の最初の草稿を私自身が書いたにもかかわらず、私自身のアプリは 1 つも移行できていないからです。 それらは今週末に変更いたしましたので、皆さんも変更してみることをお勧めします。
取り組むべきことは 2 つあります。
-
preload
スクリプトや実際のWebContents
で Node.js のコードを使用している場合は、Node.js とのやり取りをすべてメインプロセス (言うなればユーティリティプロセス) に移す必要があります。 レンダラーがの強力さを考えると、コードの大部分はリファクタリングの必要がない可能性が高いです。プロセス間通信 のドキュメントもご参照ください。 私の場合、多くのコードを移動させて
ipcRenderer.invoke()
とipcMain.handle()
でラップすれば、メインプロセスは簡単ですぐに終わりました。 ここであなたの API 設計にはお気をつけください。もしexecuteCodeAsRoot(code)
のような API を作成したら、サンドボックスはユーザーを守れなくなります。 -
サンドボックスを有効にすると、プリロードスクリプトの Node.js 統合が無効になるため、
require("../my-script")
を使用できなくなります。 つまり、プリロードスクリプトは単一のファイルである必要があります。この実現には複数の方法があります。Webpack、esbuild、parcel、rollup はすべて、この役割を担います。 私は Electron Forge の優れた Webpack プラグイン を使いました。同じくらい人気のある
electron-builder
のユーザーの方は、electron-webpack
を利用できるでしょう。
これで全てです。Webpack の巨大なパワーを使いこなすために頭を悩ませたことや、他の方法へコードをリファクタリングする機会にしようと決めたことも含めると、全体の処置には 4 日ほどかかりました。