WebViewとハイブリッドブリッジ
Web資産を1本のアプリに載せて開発を速めたい。WKWebViewとAndroid WebViewのJSブリッジで、ネイティブとJSを安全に双方向通信させる原理が分かる。
- WKWebViewは別プロセスで動くため、WebのJSからネイティブを叩くにはメッセージハンドラという非同期の一方向チャネルが唯一の正規経路になる。
- Android WebViewのaddJavascriptInterfaceは同一プロセスでJavaメソッドを直接公開でき強力だが、公開面がそのまま攻撃面になるため@JavascriptInterface注釈で最小化する。
- ブリッジの安全性はオリジン検証に集約される。メッセージ受信側で送信元URLのスキーム・ホストを都度照合し、想定外オリジンからの呼び出しを破棄することが必須。
ハイブリッドアプリの越境問題
ハイブリッドアプリは、UIの一部または全体をHTML/CSS/JavaScriptで作り、それをネイティブのWebViewに埋め込んで動かします。狙いは、Web資産の再利用とプラットフォーム横断の開発効率です。ただしWebViewの中で動くJavaScriptは、既定ではカメラ・連絡先・生体認証といった端末機能に触れられません。それらはネイティブAPIの領分だからです。したがってハイブリッドアプリの心臓部は、WebViewの中のJSと、その外側のネイティブコードとをどう安全に双方向通信させるかという「ブリッジ」の設計に集約されます。この越境の仕組みは、/mobile-development/native-bridge-jni-jsi/で扱うJS↔ネイティブFFIと発想が近く、シリアライズと非同期性という同じ課題を抱えます。
WKWebView:別プロセスとメッセージハンドラ
iOSのWKWebView(WebKit)は、旧世代のUIWebViewと決定的に違い、Webコンテンツのレンダリングとネットワーク処理をアプリ本体とは別のプロセス(WebContentプロセス)で実行します。これによりWeb側でクラッシュや脆弱性を突かれてもアプリ本体のプロセス空間を直接侵せず、隔離が保たれます。この別プロセス構造ゆえに、JSとネイティブは同じアドレス空間のオブジェクトを共有できず、通信は必ずプロセス境界を越えるメッセージパッシングになります。
横にスクロール
正規の経路が「メッセージハンドラ」です。ネイティブ側はWKUserContentControllerにハンドラ名を登録し、JS側はwindow.webkit.messageHandlers.<名前>.postMessage(値)で値を送ります。ネイティブ側はWKScriptMessageHandlerプロトコルの受信メソッドでその値を受け取ります。
// ネイティブ側:ハンドラ登録
let controller = WKUserContentController()
controller.add(self, name: "nativeBridge")
// 受信: func userContentController(_:didReceive message:)
// message.body に JS からの値、message.frameInfo に送信元情報
// JS 側:ネイティブへ送信(非同期・戻り値なし)
window.webkit.messageHandlers.nativeBridge.postMessage({
action: "getContacts"
});
ここで押さえるべき原理は二つあります。第一に、この経路は本質的に非同期かつ一方向です。postMessageは戻り値を返さないため、ネイティブからの応答をJSで受け取るには、ネイティブ側からevaluateJavaScriptで明示的にJS関数を呼び戻すか、Webページに用意したコールバックを叩く、という逆方向の経路を別途組む必要があります。第二に、message.bodyに載せられる値はプロパティリスト互換の型(文字列・数値・配列・辞書・真偽値など)に限られ、複雑なオブジェクトはこの範囲で表現できるようシリアライズが要ります。
WKWebViewのブリッジには「JS→ネイティブ」と「ネイティブ→JS」で独立した仕組みがあります。JS→ネイティブはpostMessageによるメッセージハンドラ、ネイティブ→JSはevaluateJavaScriptによるスクリプト注入です。両者を組み合わせて初めて要求と応答が成立します。実務ではJS側で採番したリクエストIDをpostMessageに含め、ネイティブが処理後に同じIDを添えてコールバックを呼ぶことで、非同期な要求と応答を対応付けます。
Android WebView:直接公開の強力さと危うさ
AndroidのWebViewは歴史的にアプリと同一プロセスで動作し、ブリッジの発想もiOSと異なります。中核はaddJavascriptInterfaceで、Javaオブジェクトをそのままグローバルスコープの名前としてJS側に露出させます。JSからは公開されたメソッドをAndroid.メソッド名()のように直接呼べ、しかも戻り値を同期的に受け取れます。
// ネイティブ側:Java オブジェクトを JS に公開
webView.addJavascriptInterface(new Bridge(), "Android");
class Bridge {
@JavascriptInterface // これが無いメソッドは JS から見えない
public String getToken() { return session.token(); }
}
この直接公開はWKWebViewのメッセージハンドラより表現力が高い一方、公開したメソッド群がそのまま外部からの攻撃面になります。歴史的に、@JavascriptInterface注釈が必須化されるAPIレベル17(Android 4.2)より前は、公開オブジェクトの全publicメソッドがJSから可視で、リフレクション経由でRuntime.execに到達し任意コード実行を招く重大な脆弱性(CVE-2012-6636)が知られていました。現在は@JavascriptInterfaceを付けたメソッドだけが露出し、公開面を明示的に最小化する設計に改められています。
addJavascriptInterfaceで公開したメソッドは、そのWebViewが読み込むあらゆるコンテンツから呼べます。もし信頼できない外部ページやiframeを同じWebViewで開けば、そのページのJSも公開メソッドを叩けてしまいます。公開するのは必要最小限のメソッドに絞り、認証トークンの取得やファイル書き込みなど機微な操作は直接ブリッジに載せない、あるいはネイティブ側で厳格に検証してから実行することが原則です。
二つのブリッジ設計の対比
WKWebViewとAndroid WebViewは、同じ「JS↔ネイティブ」という課題に対して異なる既定を選んでいます。この違いは、iOSがプロセス隔離とセキュリティを優先し、Androidが同一プロセスでの表現力を優先してきた歴史を反映しています。
| 観点 | WKWebView(iOS) | Android WebView |
|---|---|---|
| プロセスモデル | WebContentは別プロセスで隔離 | 既定でアプリと同一プロセス |
| JS→ネイティブ | postMessageによる非同期メッセージ | 公開Javaメソッドの同期直接呼び出し |
| 戻り値 | 同期戻り値なし。逆方向経路で応答 | メソッド戻り値を同期取得可能 |
| ネイティブ→JS | evaluateJavaScriptで注入 | evaluateJavascriptで注入 |
| 主な攻撃面 | 注入スクリプトと受信値の検証 | 公開メソッド群そのもの |
いずれの設計でも、ネイティブ→JS方向は共通してスクリプト注入(evaluateJavaScript/evaluateJavascript)で行います。ここでネイティブからJSへ値を埋め込むとき、値を無検証で文字列連結してJSコードを生成すると、値の中に閉じ引用符やスクリプト片が混入した際にJSインジェクションを許します。注入する値は必ずJSONエンコードなどでエスケープし、コードではなくデータとして渡すことが要点です。この双方向の関係は、/mobile-development/mobile-view-rendering-pipeline/で述べるネイティブUIの描画とは独立した、WebView内部の別レイヤーで動きます。
オリジン制限:ブリッジ安全性の要
ブリッジのセキュリティは、最終的に「その呼び出しは信頼できる送信元から来たか」の検証に帰着します。WebViewが自社が管理するローカルアセットや特定ドメインだけを読み込む前提でも、リダイレクト・広告iframe・読み込んだページ内のXSSなどにより、想定外のオリジンのJSがブリッジを叩く余地が生まれます。したがって受信側でのオリジン検証が防御線になります。
WKWebViewでは、受信メッセージのmessage.frameInfo.securityOriginやrequest.urlから送信元のスキーム・ホスト・ポートを取得し、許可リストと照合できます。AndroidでもWebViewClient.shouldOverrideUrlLoadingやshouldInterceptRequestで読み込むURLを検査し、公開ブリッジを呼べるページ自体を信頼ドメインに限定します。共通する原則は、httpとhttps、あるサブドメインと別サブドメイン、file://とネットワークオリジンを厳密に区別し、完全一致に近い基準で許可することです。ホスト名の部分一致(末尾一致の緩い判定など)はevil-yourbank.comのような紛れを通しかねず危険です。
Webのセキュリティ境界であるオリジンは、スキーム・ホスト・ポートの三つ組で定義されます。ブリッジの許可判定でこのいずれかを緩めると境界が崩れます。とくにローカルコンテンツをfile://で読み込む構成は、file://が広範なローカルファイルへの読み取りとみなされうるため扱いが難しく、iOS・Androidともに専用のローカルスキーム(カスタムスキームハンドラやアプリ内アセット配信)でコンテンツを供給し、オリジンを明確に固定する方式が推奨されます。この隔離の考え方は/mobile-development/mobile-app-sandbox-permissions/のサンドボックス原則と地続きです。
多層で守るなら、オリジン検証に加えてブリッジ経由で呼べる操作自体を最小権限に絞り、機微な操作は都度ネイティブ側で引数を検証し、必要ならユーザー確認を挟みます。ハイブリッド構成そのものの得失は/mobile-development/cross-platform-rendering-strategies/で扱う描画戦略の一類型として捉えると位置づけが明確になります。
まとめ
WebViewとネイティブのブリッジは、WKWebViewでは別プロセス構造ゆえの非同期メッセージハンドラ、Android WebViewでは同一プロセスでの直接メソッド公開という、異なる既定から出発します。前者はpostMessageとevaluateJavaScriptという二つの片道経路を組み合わせて双方向通信を成立させ、後者は@JavascriptInterfaceで公開面を絞った同期呼び出しを提供します。どちらの方式でも安全性の要はオリジン検証にあり、スキーム・ホスト・ポートの三つ組を厳密に照合して想定外の送信元からの呼び出しを破棄し、公開する操作を最小権限に保つことが、ハイブリッドアプリを堅牢に保つ共通の原則です。
モバイル開発の記事ガイド
WebViewとハイブリッドブリッジを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
WebView
比較で見る軸
難易度: advanced / カテゴリ: モバイル開発 / タグ数: 6
導入後に効く点
Android WebViewのaddJavascriptInterfaceは同一プロセスでJavaメソッドを直接公開でき強力だが、公開面がそのまま攻撃面になるため@JavascriptInterface注釈で最小化する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- モバイル開発
- タグ数
- 6
判断チェックリスト
- 自社の用途が「WebView / ハイブリッドアプリ」に近いか確認する。
- 強みである「WKWebViewは別プロセスで動くため、WebのJSからネイティブを叩くにはメッセージハンドラという非同期の一方向チャネルが唯一の正規経路になる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。