ククログ

株式会社クリアコード > ククログ > WebExtensionsでの裏技的な「スクリプトを後から注入」用法が使えなくなった件

WebExtensionsでの裏技的な「スクリプトを後から注入」用法が使えなくなった件

結城です。

Firefox 149での開発者向けの変更点情報に記載されたトピックの1つに、拡張機能内のリソース(URLがmoz-extension://~となる物)を読み込んだタブへのスクリプトの動的な注入操作の廃止があります。 この変更は現在は開発版でのみ有効化されており、Firefox 152以降でリリース版でも有効化される旨が決定しています。 今のところは隠し設定を切り替えれば従来の動作に戻せます1が、いずれはこの隠し設定も廃止されて、拡張機能内のリソースに対してはスクリプトの動的な注入操作が恒久的に不可能になるはずです。

筆者が開発している拡張機能「Tree Style Tab(TST)」や、そこで使用しているライブラリーを共用している当社製の他の拡張機能でも、一部の機能が動かなくなる影響が出ていたため、この2ヵ月ほどの間に対応のための改修を行いました。 この記事では、「そもそも、それらの拡張機能ではなぜスクリプトの動的な注入を行っていたのか」「なぜスクリプトの動的な注入が禁止されるようになったのか」「変化に拡張機能側でどのように対応したのか」を解説します。

そもそも、なぜスクリプトの動的な注入を行っていたのか

通常、スクリプトの動的な注入・実行機能は、いわゆるユーザースクリプト機能を実現したり、一般のWebページの内容を拡張機能が変更したり、拡張機能が提供する独自のUIをWebページ内に埋め込んだりするために使われます。

ただ、拡張機能のパッケージ内に含めたHTMLファイル(moz-extension://で始まるURLが自動的に割り当てられる)からは、わざわざ動的に注入しなくても、普通に<script>を使ってパッケージ内のJavaScriptファイルを読み込めます。 拡張機能で独自のUIをウィンドウやタブの形で提供する場合は、そうしてスクリプトを読み込むように記述したHTMLファイルのURLをtabs.create()tabs.update()windows.create()などに指定して開くだけで済みます。 つまり、ウィンドウやタブで独自のUIを提供する際に「ウィンドウやタブを開いて、後からスクリプトを動的に注入する」という操作は、普通、する必要がありません

それにも関わらずTSTや当社製の拡張機能が敢えてスクリプトの注入という手法を用いていたのは、それが拡張機能の技術的な制約を回避する「裏技」として有効だったからです。 どういうことか、詳しく説明しましょう。

裏技が必要な事例1:拡張機能がアンロードされるとタブやウィンドウが自動的に閉じられてしまう問題

Firefoxは、拡張機能独自のUIの提供用にmoz-extension://で始まるURLのリソースを開いたブラウザーのタブやウィンドウは、基本的に、拡張機能がアンロードされた際に道連れで破棄されるようになっています。

TSTはタブ同士の親子関係をツリー状に表示する拡張機能ですが、その性質上、複数のタブをグループ状にまとめるためにはどうしても「親になるタブ」が必要となります2。 そのためTSTでは「グループの親タブとして使うためだけのタブ(グループ化用のタブ)」を提供していますが、その実態は moz-extension://<UUID>/resources/group-tab.html というURLでアクセス可能なHTMLファイルとなっています。 よって、普通に実装すると、TSTが自動更新されるときやFirefoxを再起動するときなどにTSTがアンロードされた際、グループ化用のタブがFirefoxによって閉じられ、タブのツリー構造が崩れるという問題が起こります。

これを防ぐために有効な「裏技」が、<script>を記述せずに作っておいた静的なHTMLファイルをタブで開いて、後からそのタブに「グループ化用のタブとしての振る舞い」を実装するスクリプトを注入するというやり方でした。 この方法を使うと、TSTがアンロードされたときに破棄される対象がタブ全体ではなく後から注入されたスクリプトのみとなり、タブは(動作しない状態にはなるものの)TSTのアンロード後も残るようになります。

しかし、冒頭で述べたFirefoxの変更によってスクリプトの注入が禁止されたことによって、グループ化用のタブをUIとして動作させるために必要なスクリプトが読み込まれなくなった結果、グループ化用のタブが機能しなくなってしまいましたTSTと似たジャンルの拡張機能であるSideberyのイシュートラッカーにも同様の報告があり、そこにFirefoxの開発者の方が説明のコメントを寄せていたのを見て、筆者は今回の件の詳しい事情を把握した次第でした。

裏技が必要な事例2:独自UIのためのウィンドウが「閉じたタブ・ウィンドウを開き直す」対象に含まれてしまう問題

Firefoxでは、セッション復元機能の働きによって、閉じたブラウザーウィンドウやタブを一定回数分まで開き直す(復元する)ことができます。 これは拡張機能が独自のUIを提供するために開いたウィンドウも例外ではありません。 そのため、例えば「タブを閉じる前にYes/No式の確認ダイアログを表示する」機能を拡張機能が提供していた場合、以下のようなことが起こります。

  1. ユーザーがタブを閉じる操作を行う。
  2. 拡張機能が確認ダイアログ用のウィンドウを開く。
  3. ユーザーが操作の続行を指示する。
  4. 2で開かれたダイアログ用のブラウザーウィンドウが閉じられる。
  5. 1で指示されたタブが閉じられる。
  6. ユーザーがタブを開き直す操作を行う(Ctrl-Shift-T)。
    • →5で閉じられたタブが復元される。(期待通り)
  7. ユーザーが再度、タブを開き直す操作を行う(Ctrl-Shift-T)。
    • 4で閉じられた(2で開かれた)ダイアログ用のブラウザーウィンドウが復元される。(期待に反する)

拡張機能から「ユーザー操作で開き直せない特殊なブラウザーウィンドウ」を開ければよいのですが、残念ながらそのようなAPIはWebExtensionsにはありません3

ただし、Firefoxには「about:blank を読み込んだタブ(空のタブ)1つだけを開いていたブラウザーウィンドウだけは、ウィンドウを開き直す動作の対象にならない」という仕様があります。 これを応用すると、「まず about:blank でブラウザーウィンドウを開いて、そこにダイアログとしての振る舞いを実装するスクリプトを注入する」という手順を経ることで、前述の手順7においてブラウザーウィンドウが復元されない形でダイアログを提供できます

しかし、この方法でダイアログを提供するには、「about:blank のタブにスクリプトを注入・実行するための <all_urls> のホスト権限が必要となる」という、また別の問題があります。

<all_urls> を要求する拡張機能は、インストール時に「すべてのウェブサイトの保存されたデータへのアクセス」を求めているものとして表示されるため、ユーザー視点では「謳っている機能とは無関係に過大な権限を要求する怪しい拡張機能」という見え方になりかねません。 Tree Style Tabも当初は <all_urls> を常に要求する仕様でしたが、この点が問題になったためにオプトインに切り替えた経緯があります[^opt-in]。

セキュリティリスクの低減と機能性とを両立するため、TSTは「ユーザーが <all_urls> の権限をオプトインで与えた場合は完全に動作する」「そうでなければ、不完全でもそれなりに使えるように動作する」という2つの動作モードを持っています。 そして、<all_urls> の権限が与えられていないときに「タブを開き直す操作で意図せずダイアログ用のブラウザーウィンドウが復元されてしまう問題は起こるが、ダイアログは機能する」という代替動作を実現するために、TSTは <all_urls> の権限が無い場合、about:blank ではなく拡張機能内に組み込んだ静的なHTMLファイルの moz-extension://<UUID>/resources/blank.html を開き、そこにスクリプトを注入するようになっていました。 この処理が冒頭で述べたFirefoxの変更の影響によって動作しなくなり、その結果、<all_urls> の権限を与えていない環境では独自のダイアログが動作しない(空のウィンドウが開かれて終わってしまったり、そもそもダイアログ自体が開かれなかったりする)、という問題が起こるようになっていたのでした。

ちなみに、Thunderbirdでは拡張機能が開いたブラウザーウィンドウは復元の対象にならないため、「ダイアログのウィンドウを復元させないためにウィンドウを about:blank で開く」ということをそもそもする必要がありません。 そのためThunderbird用拡張機能では、常にこの「<all_urls> の権限が無い場合の動作」が行われることになり、そのままだとThunderbird 152以降で「ダイアログが動作しない」問題が顕在化することになります。

なぜスクリプトの動的な注入が禁止されるようになったのか

これらの「裏技」は、「拡張機能でコンテンツ内にスクリプトを注入するためには manifest.json で許可対象のホストを明示しておく必要があるが、URLが moz-extension:// で始まるその拡張機能自身のリソースでなら、特に何の権限も要求せずともスクリプトを注入・実行できる」というFirefoxの性質を前提として成立していました。

しかしBug 2011234には、この動作は当初は意図的なものではなかったということが記されています。 時系列に整理された経緯は、以下の通りです。

  1. Firefox 55の時点では、(Firefox 152以降と同様に)「権限が無いのでスクリプトを実行できない」というエラーになる仕様だった
  2. Firefox 56の時点で、 webRequest APIのための別の変更の副作用によってエラーにならなくなった。
  3. Firefox 58の時点で、「権限なしでスクリプトを実行できるのがFirefox 56以降での仕様である」と追認された
  4. その後のManifest V3対応の過程で、文字列として与えられたスクリプトの注入・実行が厳しく制限されるようになってきた結果、それらの方針との間で矛盾が生じるようになった。

この整理を踏まえて、Firefox 55以前と同様にスクリプトの注入・実行を禁止することが改めて決定された、というのが今回の変更です。

そういうわけなので、今後「スクリプトの注入禁止を撤回する」という判断が下る可能性は極めて低く、拡張機能の開発者は「制限を迂回してFirefoxに従来通りの動作をさせる」方向ではなく、新しい仕様に追従して「制限された動作の範囲内で目的を達成できる別の方法を見付ける」ほかない、ということになります。

スクリプトの注入禁止という変化に拡張機能側でどう対応したのか

Firefox 152以降で引き続き拡張機能の動作を維持するためには、スクリプトの注入を使わずに目的を達成する必要があります。 TSTでは前述の2つのケースについて、それぞれ異なる対応を行いました。

事例1:グループ化用のタブのスクリプトで拡張機能用のAPIを一切使わないようにする

裏技の用例1は、「<script> でスクリプトを読み込ませるとタブが自動的に閉じられてしまうので、そうならないようにスクリプトを後から注入する」というものでした。 この「タブが自動的に閉じられてしまう」という結果は、実は<script> でスクリプトを読み込ませること」ではなく「<script> で読み込んだスクリプトがWebExtensions APIに触れていること」が原因で起こっているという事実が、Firefoxの開発者の方のコメントによって明らかになっています。

であれば、WebExtensions APIが必要な特別な処理はすべてバックグラウンドスクリプトの側で代行するようにして、グループ化用のタブのスクリプトは「バックグラウンドスクリプトに処理を依頼する」「バックグラウンドスクリプトから処理の結果を受け取る」だけにしてしまえば、スクリプトを後から注入せずとも <script> で安全に読み込ませられるはずです。

ただ、そのためにはどうしても、グループ化用のタブ内に読み込まれた非特権スクリプトと、特権を持つバックグラウンドスクリプトとの間で密な通信を行う必要があります。 通常であれば runtime.sendMessage() というWebExtensions APIを使う所ですが、今回の要件を満たすためには、どうしてもこのAPIなしで通信しなくてはなりません。

そのために利用できるのが、Sideberyの開発者の方が先のコメントを承けて言及されているBroadcastChannel というWeb APIです。 ただ、BroadcastChannelruntime.sendMessage()(と runtime.onMessage)での通信とは仕様が大きく異なるため、対応には手間がかかります。 また、筆者が検証した限りでは、「プライベートブラウジングウィンドウや別コンテナーのタブとの間での通信には使えない」という制限事項もありました。

そこで筆者が開発したのが、webextensions-lib-cross-context-messaging という拡張機能向けライブラリーです。 このライブラリーは以下の2つのことを行います。

  1. runtime.sendMessage() と互換性のあるAPIでwrapして、拡張機能の改修コストを低減する。
  2. バックエンドとして BroadcastChannel を使える場合はそちらを使い、そうでない場合は、URLのハッシュ部分を使った代替の通信方法4を使う。

このライブラリーによって、TST側は最小限の変更でFirefox 152以降の仕様に対応することができました5

事例2:スクリプト注入なしでもダイアログとして動作するように設計を変更する

裏技の用例2では、独自のダイアログを「常にスクリプト注入で実装を読み込ませて動作を実現する」形式から、

  • about:blank でスクリプトを実行できる権限がオプトインで与えられていない場合は、静的なHTMLファイルのURLを指定してウィンドウを開き、そのHTMLファイルから静的に読み込んだスクリプトでダイアログとしての動作を実現する。
    • 拡張機能からのパラメーター引き渡しには、HTMLファイルを読み込むURLの末尾に付与したクエリーパラメーターを使用する
  • 権限がオプトインで与えられている場合は、about:blank でウィンドウを開いてから、静的なHTMLファイルを <iframe> で埋め込み、ダイアログとしての動作を実現する。

というハイブリッド形式に変更して対応しました。 この変更はTST本体ではなく、webextensions-lib-rich-confirm という拡張機能向けライブラリーの側で行っています。

元々、この独自のダイアログを提供するライブラリーが「常にスクリプト注入を使う」実装になっていたのは、以下の理由からでした。

  • about:blank を使う場合を基本と考えて実装した後で、about:blank を使わない(<all_urls> 権限がオプトインされていない)場合に後から最小限の変更で対応することになった。
  • ライブラリーの実装を単一のスクリプトだけで完結させたかった。

今回の件では「拡張機能に含めた静的なHTMLファイルからは、スクリプトは静的に記述した <script> で読み込む必要がある」という前提が加わったため、about:blank を使わない場合を基本、about:blank を使う場合を例外と考えて、実装を全面的に見直した形となります。 また、そのように実装を改めた都合上、ライブラリーを構成するファイルは以下の3つに分割されました。

  • 全体を制御する RichConfirm.js
  • ダイアログ提供用の静的なHTMLファイル RichConfirmDialog.html
  • ダイアログの動作を実装する静的なスクリプト RichConfirmDialog.js

RichConfirm.js は従来とほぼ同じAPIを維持していますが、コールバックの扱いなどで仕様が一部変わっていますので、利用する拡張機能の側にも改修が必要となっています。

まとめ

以上、Firefox 149(Firefox 152)以降での仕様変更が拡張機能に及ぶ影響の詳細と、拡張機能の改修による対応の事例を紹介しました。

株式会社クリアコードは、FirefoxやThunderbirdの法人運用において生じた様々な問題を解決する手段の1つとして、拡張機能の開発も承っています。 また、FirefoxやThunderbirdの仕様変更を早めにキャッチアップし、可能な限り問題が顕在化する前の時点での対応を行うように努めています。

FirefoxやThunderbirdの運用で何かお困りの企業の運用担当者さまは、お問い合わせフォームよりお問い合わせ下さい。

  1. extensions.webextensions.allow_executeScript_in_moz_extensionabout:configなどを用いてtrueに変更します。

  2. 最近のバージョンのTSTではFirefox本体に入ったタブグループ機能に対応しており、「親のタブ」を持たないグループも表示できるようになっています。

  3. Firefox 149以降では、ツールバーボタンなどから開けるポップアップパネルをユーザーの操作なしに開けるようになったため、これをダイアログの代わりに使う方法をとれます。

  4. メッセージをチャンクに分割してURLのハッシュ部分に載せ、TCPと同じ要領で双方向に通信します。URLの変更が非同期で通知される都合上、BroadcastChannel と比べて通信速度はだいぶ低速になります。

  5. ちなみに、「拡張機能のアンロード後もタブを残す」という裏技の前提の1つである「拡張機能のAPIを使うスクリプトが読み込まれていなければ、拡張機能のアンロード後もタブが残る」動作自体は、本来は望ましくないものであるらしく、そのようなタブも残さず閉じるようにするべきというbugが起票されています。今回影響を受けることになったスクリプトの注入の禁止措置と同様、このbugが「修正」された時には、また何らかの対応が必要になるものと予想されます。他方で、拡張機能のタブをFirefoxの再起動後も残すようにして欲しいという要望も起票されており、仕様の穴を突いた裏技でない、何らかの正式な代替手段が提供されるようになると期待したいところです。