【ブラウザ拡張】manifest v2 と v3 で実行可能な eval を作る

ブラウザ拡張機能作っていますか?そこで eval を実行したいというケースは多くあると思います。

例えば、 options ページにてユーザーが任意のコードを入力できるようにしてそれを実行するケースなどです。任意の JavaScript コードを実行する際には evalnew Function を使いますが、ブラウザ拡張機能では簡単に使うことはできません。特に、 Manifest V3 ではよりセキュリティが向上しており単体での eval とそれ相応の機能はほぼ使用不可*1となっています。

これらの制約下でもどうしても eval を実行できるようにする方法を解説します。

この解説では拡張機能フレームワークである WXT を使用しています

ブラウザ拡張機能は manifest.json とそれに付随する js や html のファイル郡として構成されています。このため、通常であればフレームワークなどは必要無いのですが、違う Manifest バージョンを共通させたい場合、特に Chrome と Firefox の拡張機能を同時に開発したい場合はフレームワークを使うのが最善かと思います。

ブラウザ拡張機能のフレームワークで有名なのは Plasmo がありますが、今回は個人的な好みで WXT を使用したケースで解説を行います。WXT は Vite ベースのフレームワークでとても扱いやすいのでぜひ触ってみてください。

リポジトリ公開しています

ご自由にお使いください。

github.com

manifest v2 と v3 に対応する

Manifest は現在 v2 と v3 が存在します。Chrome は 2024年の6月に v3 に完全移行され v2 は非推奨になったのはニュースで知っている方は多いと思います。

adguard.com

developer.chrome.com

しかし、古いバージョンのブラウザに互換を持たせたい場合などもあるかと思います。WXTは manifest v2 と v3 に対応しており、これらは wxt.config.ts で出し分けなどが簡単に設定できます。

eval の実行は v2 では manifest.jsoncontent_security_policy: "script-src 'self' 'unsafe-eval'; object-src 'self';" が必要ですが、 v3 では sandbox で実行するため、content_security_policy: {sandbox: "sandbox allow-scripts; script-src 'self' 'unsafe-eval';"} が必要になります。これらを出し分けるため、 wxt.config.ts を以下のように記述します。

import { defineConfig } from "wxt";

// See https://wxt.dev/api/config.html
export default defineConfig({
  manifest: (config) => {
    const contentSecurityPolicy =
      config.manifestVersion === 3
        ? {
            sandbox: "sandbox allow-scripts; script-src 'self' 'unsafe-eval';",
          }
        : "script-src 'self' 'unsafe-eval'; object-src 'self';";

    return {
      content_security_policy: contentSecurityPolicy,
    };
  },

  modules: ["@wxt-dev/module-react"],
});

manifest に関数を渡すとconfigを引数に取ることができます。この config から適用する manifest のバージョンを知ることができるのでこれを使用して設定の出し分けを行うことができます。

sandbox を作成する

WXT は sandbox を作るのは非常に簡単です。entrypoints/sandbox/index.html を作成するだけで sandbox を作ることができます。なお、v2設定でビルドすると sandbox は成果物に含まれません。

wxt.dev

eval を実行する際にはこの sandbox の html を iframe で読み込みつつ、 PostMessage 経由でやり取りをします。

今回私の例では React の useEffect 内で PostMessage を使用していますが、React である必要は無いはずなので生の TypeScript を記述してしまったほうが良さそうです。

github.com

eval 呼び出し

さて、先程つくった sandbox を呼び出す処理を書きます。以下のようにリスナーを設置して返ってくるデータを受け取れるようにします。

WXT では sandbox はデフォルトでは ./sandbox.html に設置されるのでこれを iframe の src に指定するだけで呼び出すことが可能です。

export function App() {
  const iframeRef = React.useRef<HTMLIFrameElement>(null);

  React.useEffect(() => {
    if (!iframeRef.current) {
      return;
    }

    const listener = (event: MessageEvent) => {
      const data = event.data;
      if (data.type !== "sandbox-eval-result") {
        return;
      }

      setResult(data.result);
    };

    window.addEventListener("message", listener);
    return () => {
      window.removeEventListener("message", listener);
    };
  }, []);

  return <iframe src="./sandbox.html" ref={iframeRef} />;
}

送信は iframe.contentWindow?.postMessage() を使用することで送信することができます。ここあたりは iframe のメッセージ送受信でよく使われる仕様かと思います。

manifest v2 の場合は iframe を使わない

さて、先程も行った通り manifest v2 は sandbox は使えません。そのため、 iframe も使えないので別の方法を取る必要があります。

WXT では import.meta.env.MANIFEST_VERSION とすることでコード内から manifest のバージョンを取得できます。これを使用して iframe を出し分けするのが簡単そうです。

{import.meta.env.MANIFEST_VERSION === 3 && (
  <iframe src="./sandbox.html" ref={iframeRef} />
)}

こうすることで、 manifest v2 の場合は iframeRefnull となるのでその際は直接 eval を呼び出すと良さそうです。

github.com

だけど、最近は manifest v2 非対応でいいと思う

ここまで長く解説してきましたが、ぶっちゃけ現在は Chrome も Firefox も manifest v3 対応しているのでぶっちゃけ出し分けする必要はなさそうです。

あと、できるだけ eval は使わないに越したことは無いです。

*1:sandbox のみ利用可能