ククログ

株式会社クリアコード > ククログ > ブラウザーの拡張機能でコンテンツ内に安全に情報を埋め込む方法のベストプラクティスとバッドプラクティス

ブラウザーの拡張機能でコンテンツ内に安全に情報を埋め込む方法のベストプラクティスとバッドプラクティス

Firefoxの拡張機能開発で育ち、今はChrome/EdgeなどのChromium系ブラウザー向け拡張機能も開発している結城です。

これらのブラウザー向け拡張機能では、拡張機能が能動的にUIを表示する方法が限られています1。能動的に任意の位置にUIを表示したい場合、windows.create()でウィンドウを開くか、コンテントスクリプトを使ってコンテンツ内にUIを埋め込むかのどちらかの方法を取る必要があります。 この記事では、後者の「コンテンツ内にUIを埋め込む」やり方について、現時点でのベストプラクティスと、それ以外の方法の問題点を紹介します。

最初に結論だけ述べると、mode:'closed'Shadow DOMを使い、画像などは拡張機能のパッケージ内に含めておくのが現状でのベストで、それ以外の方法は全てセキュリティまたはプライバシー保護の点で問題があります

前提:どういう時に必要な技術か

冒頭の説明だけではそもそも何故そんなことをする必要があるのか分からないと思うので、もう少し背景を説明します。

筆者は個人的に、Firefoxのサイドバーにタブをツリー表示する拡張機能「Tree Style Tab」を開発しています。
(画像:Tree Style Tabのスクリーンショット)
この拡張機能はFirefoxのタブUIの機能を可能な限り再現する方針で開発していますが、技術的な制約により再現できていない機能もいくつかあります。つい最近までその中の1つとして、「タブのプレビューのポップアップ」がありました。

どのような機能かというと、Firefoxのタブの上にマウスのポインターを移動してしばらく待つと、タブの下にポップアップパネルが自動的に開かれ、その中にページのタイトルやサムネイル画像が表示されるというものです。
(画像:Firefoxのタブのプレビューのスクリーンショット)
このスクリーンショットを見て分かるとおり、プレビューのポップアップパネルはFirefoxのツールバーやコンテンツ領域の上に重なるように表示されます。 他方で、Tree Style Tabは専用のサイドバーパネルの中にUIを表示する設計のため、そのままではサイドバー内にしかUIを表示できず、「コンテンツ領域の上に重なるようなポップアップパネル」は実現できません。 そのため、実装されたタブのプレビューは、コンテンツ領域内に埋め込む形で実現しています。
(画像:Firefoxのタブのプレビューのスクリーンショット)

このようなことをTree Style Tabのような拡張機能でやるにあたって、特に重要なポイントとして以下の2点があります。

  1. いかにして見た目・振る舞いをFirefox本来のUIに似せるか。
  2. いかにしてセキュリティ・プライバシー的に安全に機能を実装するか。

まず1点目について。 WebExensions APIではウィンドウを開く方法は windows.create() しか存在せず、このAPIでは「タイトルバーがある矩形のウィンドウ」よりもUI要素が少ないウィンドウは開けません。 また、少なくともWindows版のFirefoxでは、このAPIで開かれたウィンドウは必ずフォーカスを奪ってしまう2という制限事項もあります。 タブの上にカーソルを移動するだけでごついウィンドウが開かれ、しかもウィンドウのフォーカスが切り替わってしまっては、現実的には使用に耐えません。 よって、選択肢としては「コンテンツスクリプトを使って、コンテンツ領域内にUI要素を埋め込み、それをスタイルシートでポップアップパネル状に調整する」以外の方法がないことになります。

そこで問題になるのが2点目の安全性です。 何も考えずにWebページ内にコンテンツを埋め込んだ場合、それはWebページ内のスクリプトから読み取れる物になります3。 「マウスカーソルでポイントしたタブのプレビューをポップアップパネルで表示する」UIをそのままWebページに埋め込むということは、現在閲覧中のタブのWebページ内に他のタブの情報を埋め込むということです。 Webページのスクリプトからそれらの情報を読み取れてしまうと、ユーザー固有の情報がセキュリティ境界を越えて漏洩しうる、つまり、ユーザーを危険に晒す脆弱性がある4ということになります。

以上の背景から、コンテンツ内にUIを埋め込むにあたり、いかにして攻撃者から内容を読み取られにくくするか、という観点でベストプラクティスとそれ以外の方法とを紹介するのがこの記事の主題です。

安全性の面でのベストプラクティス

冒頭でも結論として述べていますが、セキュリティとプライバシーの面で安全を保つためには、以下の2点を守るのが本稿執筆時点でのベストプラクティスと言えます。

  1. UIはカスタム要素のShadow DOMの中に埋め込む。
  2. 画像などのリソースはWeb上のものを直接参照せず、拡張機能のパッケージ内に含めておく。

順に説明しましょう。

カスタム要素のShadow DOMでUI要素を埋め込む

現代のブラウザーは、任意のタグ名のカスタム要素をJavaScriptを使って定義できます。 またその際に、Shadow DOMを使って「videoタグを書くだけで、その中にシークバーや再生・停止ボタンなどのUI一揃いを含めた状態にする」ようなこともできます。 この方法を使うと、Webページのスクリプトからは内容にアクセスできない状態のUI要素をWebページに埋め込むことが可能です。 以下は、実際にカスタム要素を使って情報を安全に埋め込む例です。

// コンテンツスクリプトとして実行する。
// tab として別のタブのオブジェクト(tabs.Tab)が、
// previewとしてサムネイル画像のData URIが渡されてきたと仮定する。

// カスタム要素の名前をランダムに決める。
const elementName = (() => {
  const alphabets = 'abcdefghijklmnopqrstuvwxyz';
  const prefix = alphabets[Math.floor(Math.random() * alphabets.length)];
  return prefix + Math.round(Math.random() * Math.pow(2, 16));
})();

// コンテナーとなるカスタム要素を定義する。
class PreviewContainer extends HTMLElement {
  constructor() {
    super();

    // DOMツリーの変更を最小の回数で終わらせるため、UIのためのHTML要素は
    // DocumentFragmentなどを使用して先に一通り生成しておくとよい。
    const fragment = document.createDocumentFragment();

    // ここでUIを組み立てる。
    const style = fragment.appendChild(document.createElement('style'));
    style.setAttirbute('type', 'text/css');
    style.textContent = `
      /* Shadow DOM内でのみ反映されるスタイルシート */
    `;
    const div = fragment.appendChild(document.createElement('div'));
    div.appendChild(document.createElement('div')).textContent = tab.title;
    div.appendChild(document.createElement('div')).textContent = tab.url;
    div.appendChild(document.createElement('img')).src = preview;

    // closedな状態でShadow DOMを作成し、UIのためのHTML要素を埋め込む。
    const shadow = this.attachShadow({ mode: 'closed' });
    shadow.appendChild(fragment);
  }
}
window.customElements.define(elementName, PreviewContainer);

// カスタム要素を生成し、コンテンツ内に埋め込む。
const container = document.createElement(elementName);
document.documentElement.appendChild(container);

chrome.runtime.onMessage.addListener(message => {
  // バックグラウンドスクリプトやService Workerなどからのメッセージに基づいて
  // UI要素の表示・非表示の切り替えなどを行う。
});

重要なのは、作成するShadow DOMはmodeclosedと指定するという点です。 この状態であれば、Shadow DOM内の要素に外部からアクセスできなくなる5ため、センシティブな情報を含めても問題ありません。

closedなShadow DOMであっても、その中にiframeを置いた場合には、その分のフレームがwindow.framesで見えるようになるため、「何らかのサブフレームが埋め込まれている」という事実までは知られてしまいます。 ただ、フレームの内容やURLの情報については、クロスオリジンの制約6によりWebページ内からのアクセスが禁止されるため、このことによる情報漏洩やフィンガープリンティング7のリスクは無いと言ってよいでしょう。

なお、センシティブな情報を含まない場合であっても、フィンガープリンティングを防ぐために、拡張機能で追加するShadow DOMは常にclosedとしておくのがお勧めです。 この観点から、

  • カスタム要素の名前(Webページのスクリプトから見える)は、動的に・ランダムに決定する
  • カスタム要素自体には属性を設定しない(必要な情報はすべてShadow DOM内にだけ置くようにする)

といった工夫も併用するとよいでしょう。

画像などのリソースはWeb上のものを直接参照せず、拡張機能のパッケージ内に含める

ただ、closedなShadow DOMといえども完全に安全とは言えません。 ネットワークを介した通信によるリソースの取得を監視するPerformanceObserverを使うと、それがclosedなShadow DOM内で起こったものであっても、画像やiframeで発生した読み込みのリクエスト先URLを捕捉できてしまいます。

const observer = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    if (entry.entryType != 'resource')
      continue;

    if (entry.initiatorType == 'iframe' &&
        entry.name.startsWith('https://example.com/favicon.ico'))
      console.log(`このユーザーはExample拡張機能を使っています`);
  }
});
observer.observe({ type: 'resource', buffered: true });

そのため、外部リソースを参照するUI要素があると、プライバシー情報が漏洩するリスクがあります。

こういったリスクを軽減する方法としては、以下の2つの対策が可能です。

  1. 画像などのリソースをあらかじめ拡張機能のパッケージに含めておき、内部URLで参照する。
  2. Web由来のリソースを直接UIに埋め込む場合は、一旦バックグラウンドで読み込んだ後でData URI8をコンテンツスクリプトに渡す。

まず1つ目について。 Chrome/Edge/Firefoxの拡張機能では、パッケージ内に含まれるリソースに内部URL(chrome-extension://...moz-extension://...)でアクセスすることができます。 manifest.jsonweb_accessible_resourcesで明示的に許可を与えれば、それらをWebページ内からも参照できます。 そうなるとフィンガープリンティングの材料に利用されるリスクがあるようにも思えます9が、筆者が検証した限りでは、前述のclosedなShadow DOMを併用する限り、これら内部リソースの読み込みリクエストは捕捉できず、PerformanceObserverでの監視も行えない模様です10

機能のアイコンなどはパッケージに含めておけますが、Webページのアイコン画像のように実際にWebから取得してくる必要があるリソースは、2つ目の工夫が必要です。 Web上のリソースをimgsrciframesrcなどでそのまま参照すると、closedなShadow DOM内の要素からの読み込みであっても、リクエストが発生したURLをPerformanceObserverで捕捉できてしまいます。 そのような場合は、Service Workerやバックグラウンドスクリプトなどの安全な領域でリソースを読み込んでData URIに変換し、それをコンテンツスクリプトに渡すようにします。

// Service Workerまたはバックグラウンドスクリプトで実行する

// URLで指定したリソースのData URIを返すユーティリティ
async function toDataURL(url) {
  return new Promise((resolve, reject) => {
    try {
      const response = await fetch(url);
      const blob = await response.blob();
      const reader = new FileReader();
      reader.onloadend = () => {
        resolve(reader.result);
      };
      reader.readAsDataURL(blob);
    }
    catch(error) {
      reject(error);
    }
  });
}

chrome.scripting.executeScript({
  target: { tabId: 123 },
  func: (favicon) => {
    // ...Shadow DOMの作成を含む処理...
    const icon = document.createElement('img');
    icon.setAttribute('src', favicon);
    shadow.appendChild(icon);
    // ...
  },
  args: await Promise.all([
    toDataURL('https://www.clear-code.com/favicon.ico'),
  ]),
});

Data URIの読み込みはPerformanceObserverでの監視対象外になるため、この方法での画像読み込みはWebページのスクリプトからは検知できません11。 これにより、読み込むURLに基づくフィンガープリンティングを防止できます。

安全でないやり方

ここまでは、コンテンツスクリプトでUI要素をWebページ内に埋め込む際の、本稿執筆時点でのベストプラクティスと言える安全なやり方を紹介しました。

ところで、ベストプラクティスと言うからには、ベストでないやり方もあります。 筆者がここに辿り着くまでに調査・検討して採用を見送った方法がそうで、具体的には以下のものがありました。

採用を見送った手法 機密情報の保護(セキュリティ担保) フィンガープリンティング防止(プライバシー保護)
そのままHTML要素を埋め込む × ×
modeopenなShadow DOMを使う ×
iframeを使い、拡張機能の内部リソース(HTML)を直接埋め込む ×
iframeを使い、BlobのオブジェクトURL経由で内容を読み込ませる × ×
iframeを使い、sandbox属性を使う ×
iframeを使い、Data URIでHTMLを読み込ませる × ×
iframeを使い、srcdocでHTMLを読み込ませる × ×
iframeを使い、about:blankを読み込ませて、任意のスクリプトをその中で実行してUI要素を組み立てる × ×

本記事を読んでいる皆さんが、ベストプラクティスとその背景を知らないまま試行錯誤してうっかりやり方を思いついてしまったり、技術に明るくない人から「こうすればよいのでは」と指示されたりした場合に、「それは危険だからやってはいけない」と説明できるようにするために、(そして、もし現在、皆さんが実際にこれらのいずれかの方法を使って拡張機能を開発しているのであれば、早急に前述の安全なやり方に改修する動機になるように、)それぞれなぜ駄目なのかを解説してみます。

HTML要素を埋め込む

前提の所で述べたとおり、コンテンツ領域内に裸の状態で埋め込んだHTML要素にそのまま情報を埋め込むのは、セキュリティ的にもプライバシー的にも危険です。 例えば以下のようなやり方です。

// コンテンツスクリプトとして実行する。
// tab として別のタブのオブジェクト(tabs.Tab)が、
// previewとしてサムネイル画像のData URIが渡されてきたと仮定する。

const div = document.createElement('div');
div.appendChild(document.createElement('div')).textContent = tab.title;
div.appendChild(document.createElement('div')).textContent = tab.url;
div.appendChild(document.createElement('img')).src = preview;
div.setAttribute('style', `
  position: fixed;
  left: 10px;
  top: 100px;
  width: 300px;
  height: 200px;
`);
document.body.appendChild(div);

このような実装をしていることを攻撃者やトラッカーの作成者に知られていると、以下の要領でUI要素の埋め込み処理を監視されて、ユーザーがその拡張機能を使っている事実や、ユーザーが開いている他のタブの情報を盗み取られてしまいます。

const observer = new MutationObserver(mutations => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.tagName?.toLowerCase() == 'div' &&
          node.getAttribute('style').includes('position: fixed') &&
          node.childNodes.length == 3)
        console.log(`このユーザーはTree Style Tabを使っていて、他のタブで ${node.childNodes[1].textContent} を開いています`);
    }
  }
});
observer.observe(document.documentElement, { childList: true, subtree: true });

筆者が見た限りでは、Webページ内にUI要素を埋め込むタイプの拡張機能でこのような実装をしている例は珍しくないようです。 この例のように致命的な情報漏洩をやらかしている事例まではそう多くないとしても、フィンガープリンティングの材料になる情報をばら撒いている物は多いと思われます。

modeopenなShadow DOMを使う

Shadow DOMを使っていても、modeclosedにしていなければ、UI要素の内容は容易に盗み見ることができてしまいます。 例えば以下のようにUI要素を実装したとします。

// コンテンツスクリプトとして実行する。
// tab として別のタブのオブジェクト(tabs.Tab)が、
// previewとしてサムネイル画像のData URIが渡されてきたと仮定する。

// コンテナーとなるカスタム要素を定義する。
class PreviewContainer extends HTMLElement {
  constructor() {
    super();

    const div = document.createElement('div');
    div.appendChild(document.createElement('div')).textContent = tab.label;
    div.appendChild(document.createElement('div')).textContent = tab.url;
    div.appendChild(document.createElement('img')).src = preview;

    const shadow = this.attachShadow({ mode: 'open' });
    shadow.appendChild(div);
  }
}
window.customElements.define('preview-container', PreviewContainer);

const container = document.createElement('preview-container');
document.body.appendChild(container);

Shadow DOMの内容はchildNodesなどの通常のDOMツリー探索の機能では参照できませんが、iframeに読み込まれたドキュメントの内容にcontentWindowcontentDocumentでアクセスできるように、Shadow DOMにもshadowRoot経由でアクセスできます。 このshadowRootへのアクセスを禁止するのがmode: 'closed'なのですが、mode: 'open'と指定してしまうと、shadowRootは初期状態では丸見えとなります。 以下は、そのようにして情報を盗み取る例です。

const observer = new MutationObserver(mutations => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.tagName?.toLowerCase() == 'preview-container' &&
          node.shadowRoot.childNodes.length == 3)
        console.log(`このユーザーはTree Style Tabを使っていて、他のタブで ${node.shadowRoot.childNodes[1].textContent} を開いています`);
    }
  }
});
observer.observe(document.documentElement, { childList: true, subtree: true });

生成AIに「Webページ内にShadow DOMを使ってコンテンツを埋め込むコードを出力してください」のようにごく単純な指示を与えると、このようなコードを生成する場合があります。 筆者が試した際にも実際にそのようなコードが生成され、「カプセル化された状態で動作します」という説明文が添えられていました。 この説明を真に受けて「カプセル化されているなら安全なのだろう」とコードを採用すると、脆弱性を仕込んでしまうことになります。

iframeを使い、拡張機能の内部リソース(HTML)を直接埋め込む

iframeはWebページの中に別のWebページを埋め込む物です。クロスオリジンの制約6により、フレーム内部と外部の間では互いに情報へのアクセスが制限されるため、Webページの動作に影響を与えずに、且つ、なるべく安全に任意のUIを埋め込む方法として古くから使われてきました。 また、iframe内で動作するスクリプトはiframeが削除されればアンロードされる(と期待できる)ため、面倒な終了処理を省略して実装を単純化するのにも有用です。

// コンテンツスクリプトとして実行する。
// tab として別のタブのオブジェクト(tabs.Tab)が、
// previewとしてサムネイル画像のData URIが渡されてきたと仮定する。

const iframe = document.createElement('iframe');
iframe.src = 'moz-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/tab-preview.html';
iframe.setAttribute('style', `
  position: fixed;
  left: 10px;
  top: 100px;
  width: 300px;
  height: 200px;
`);
document.body.appendChild(iframe);

このようにする限り、フレームの内容や中で起こったことはブラウザー自身のセキュリティ機構によって保護され、Webページ側に知られることはありません。 よって、セキュリティ的には裸のHTML要素でUIを埋め込むよりもはるかに安全です。

ただ、iframesrc属性が丸見えのため、以下のように、それをフィンガープリンティングの材料にされるリスクは依然あります。

const observer = new MutationObserver(mutations => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.tagName?.toLowerCase() == 'iframe' &&
          node.src.startsWith('moz-extension://') &&
          node.src.includes('/tab-preview.html'))
        console.log('このユーザーはTree Style Tabを使っていて、他のタブを開いています');
    }
  }
});
observer.observe(document.documentElement, { childList: true, subtree: true });

iframesrcの情報は、PerformanceObserverを使って間接的に得ることもできます。

const observer = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    if (entry.entryType != 'resource')
      continue;

    if (entry.initiatorType == 'iframe' &&
        entry.name.startsWith('moz-extension://') &&
        entry.name.includes('/tab-preview.html'))
      console.log(`このユーザーはTree Style Tabを使っていて、他のタブを開いています`);
  }
});
observer.observe({ type: 'resource', buffered: true });

前述したとおり、mode: 'closed'なShadow DOMの中にiframeを置くようにすると、MutationObserverによる監視はできなくなりますし、PerformanceObserverに対しても読み込みが通知されなくなります。 Shadow DOMという技術がある現在は、iframeを直接Webページ内に埋め込む必要性は無いと言ってよいでしょう。

iframeを使い、BlobのオブジェクトURL経由で内容を読み込ませる

現代のWebブラウザーは、画像などの任意のデータにBlob URLを割り当てて、本来のURLではなくBlob用の仮のURLでアクセスすることができます。 この方法を使って、拡張機能の内部的なリソースに一時的なBlob URLを割り当てて、iframesrcにそちらを指定するようにすれば、拡張機能の内部的なURLを露出せずに済み、フィンガープリンティング対策になるのではないか? と思えるかもしれませんが、実際にはその目的では役に立ちません。

例えばFirefoxの拡張機能において、パッケージ内に含まれるリソースのURLが moz-extension://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resources/frame.html であった場合、xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx の部分はフィンガープリンティングに使われうる情報です。 このリソースを fetch() などでBlobとして読み取って、オブジェクトURLを生成すると、得られるオブジェクトURLは blob:moz-extension://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy のような物になります。 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy の部分はランダムになるのですが、肝心の xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx の部分(ユーザー固有の値)がそのまま残っているため、「ユーザー識別のための情報を隠す」という目的には失敗していることになります。

また、オブジェクトURLで読み込んだコンテンツにはクロスオリジンの制約がかからない性質があります。 そのため、この方法で埋め込んだコンテンツの内容はWebページのスクリプトから丸見えで、セキュリティ的に却って危険な状態になってしまいます。

iframeを使い、sandbox属性を使う

iframeにはsandboxという機能があります。 プログラミングの文脈においてサンドボックスとは一般的には、他から隔離された名前空間を作成して、その中でプログラムを実行することにより、プログラムの影響範囲を限定し安全性を確保する仕組みを指します。 なので、iframesandboxを使えばより安全にiframeを使えるはず……と考えるのは早とちりです。

まず、サンドボックスという概念の説明をよく読むと分かるかと思いますが、HTMLとJavaScriptの世界においてはクロスオリジンの制約6により、iframeはそれ自体がサンドボックスとして作用します。 ではiframesandboxとは一体何なのかというと、これはiframeのサンドボックスとしての作用を細かく制御する機能で、端的には「外側のWebページのセンシティブな情報を、iframe内の危険なコンテンツから守る」ための物と言えます。 sandboxを設定したとしても、その逆の「内側のWebページのセンシティブな情報を、外側のWebページの危険なコンテンツから守る」ための役には立たず、今回の用途では安全性の向上には寄与しません。 (それどころか、iframe内でのスクリプトの動作に制限が課せられるようになるため、拡張機能独自のUIを埋め込む先として使う上では不便になるだけです。)

iframeを使い、Data URIでHTMLを読み込ませる

HTMLを文字列として組み立てて'data:text/html;' + escape(...)のように連結した物をiframesrcに設定すると、ファイルとしては存在しないページを表示させることができます。 この方法を使うと拡張機能内部のURLが露出しないため、「URLでのフィンガープリンティング」の防止には有効と言えそうです。

ただし、Data URIで読み込んだコンテンツにはクロスオリジンの制約がかからない性質があります。 そのため、この方法で埋め込んだコンテンツの内容はWebページのスクリプトから丸見えで、セキュリティ的に却って危険な状態になってしまいます。 また、コンテンツそのものが特徴の抽出材料になるため、フィンガープリンティングも防げません。

iframeを使い、srcdocでHTMLを読み込ませる

iframeにはsrcdocという属性があり、この属性の値としてHTMLのソース文字列を設定すると、ファイルとしては存在しないページを表示させることができます。

この方法の利点と欠点は、Data URIを使う手法とほぼ同じです。 セキュリティの観点でもフィンガープリンティング防止の観点でも効果はありません。

iframeを使い、about:blankを読み込ませて、任意のスクリプトをその中で実行してUI要素を組み立てる

iframesrcabout:blankを設定して空ページを読み込ませておき、tabs.executeScript()などのAPIでコンテンツスクリプトを注入して実行して動的にコンテンツを組み立てる、という方法ならどうでしょうか。 この場合、URLはabout:blankという固定の物になるため、URLによるフィンガープリンティングの材料になりません。 about:blankは一般的なWebページのURLとはオリジンが異なるので、クロスオリジンの制約によって内容が外側のWebページから保護され、セキュリティ的にも安全なのではないでしょうか。

ですが実際には期待通りにはいきません。 というのも、この説で正しいのは「URLによるフィンガープリンティングの材料にならない」という点だけです。 about:blankは特殊なURLのため、実際にはクロスオリジンの制約が適用されず、コンテンツには外側のWebページから自由にアクセスできます。 よってセキュリティ的には脆弱ですし、フィンガープリンティングのための特徴も好きなように抽出できてしまいます。

まとめ

以上、Webページ内に拡張機能で任意のUI・情報を埋め込むために2025年8月現在で使用可能な各技術について、セキュリティとプライバシーの観点でお勧めの手法の紹介と、それ以外の方法の問題点を紹介しました。

最後に、ベストプラクティスも含めて本記事で紹介した各技術の性質を表にまとめます。

手法 機密情報の保護(セキュリティ担保) フィンガープリンティング防止(プライバシー保護)
mode:'closed'のShadow DOM+内部URL
mode:'closed'のShadow DOM+Data URI
mode:'closed'のShadow DOMのみ ×
mode:'open'のShadow DOMのみ ×
そのままHTML要素を埋め込む × ×
iframe+内部URL ×
iframe+Blob URL × ×
iframe+sandbox ×
iframe+Data URI × ×
iframe+srcdoc × ×
iframe+about:blank+スクリプト注入 × ×

クリアコードではFirefoxおよびThunderbirdの法人向け技術サポートを有償にて提供しており、また、ChromeやEdgeも含めたWebブラウザー向け拡張機能の開発も承っています。 企業内でのWebブラウザーの使用でお困りの管理・運用ご担当者さまは、お問い合わせフォームよりぜひご連絡ください。

  1. 拡張機能向けのAPIでは「デスクトップ通知」「(ツールバーボタンのクリック操作で開かれる)ポップアップ」「サイドバー(Firefox)」「サイドパネル(Chrome、Edge)」など拡張機能がUIを提供できる機能がいくつかありますが、ユーザー操作に対するイベントリスナーの中で直接的に呼び出した場合しか機能しなかったり、表示できるUIの形状が固定されていたりといった制限があります。

  2. 仕様では windows.create({ focused: false }) とすることでフォーカスを与えずウィンドウを開けることになっていますが、実際には必ずアクティブなウィンドウとして開かれる結果となります。

  3. 動的にコンテンツが変更されたことを検知するにはMutationObserverを使います。DOMツリーのようその変更を監視すれば、拡張機能がUI要素をコンテンツ内に埋め込んだときに、その内容を取得することができます。

  4. 攻撃者側が「Tree Style Tabの特定のバージョンを対象に攻撃する」ようにスクリプトを書く必要はありますが、本稿執筆時点でTree Style Tabのアクティブユーザー数は20万近いことを考えると、網を張って待ち構える攻撃者が現れても不思議ではないと筆者は考えています。

  5. Web開発ツールの機能を使えばそのようなShadow DOMの内容にもアクセスできますが、Webページのスクリプトから直接それらの機能を利用はできないので、安全面では問題ありません。

  6. URLのうち https://example.com:8080/(スキーム、ドメイン、ポート番号)のような基底部分(ポート番号は省略されることが多い)をまとめて「オリジン」と呼ぶが、そのオリジンが異なるコンテンツ同士で相互の情報のアクセスを禁止する、ブラウザー自体に備わったセキュリティ機構。

  7. ブラウザーの振る舞いを分析してユーザー個々人を識別すること。Shadow DOMの内容にWebページのスクリプトからアクセスできると、「このような構造のUI要素を追加するのは何々という拡張機能であるから、このユーザーはその拡張機能を使用していると分かる」といった情報を得られるため、同様の手がかりを複数組み合わせて、より高い精度でユーザー個々人を識別されるリスクがある。

  8. data:image/png;base64,iVBORw0... といった要領で、データそのものを文字列として表現したURI。この例のように、バイナリーはBase64エンコードされる。

  9. 既知の事例のURLを指定して読み込みを試みて、成功すればその拡張機能がインストールされていると判断する、という手法でのフィンガープリンティングが可能です。ただし、FirefoxではURLのホスト名部分はインストールごとにランダムに決定されるため、単純に固定のURLでのフィンガープリンティングはできないようになっています。

  10. iframesrcで内部リソースを参照するように指定したものを、Shadow DOMではなくコンテンツ内に直接埋め込んだ場合は、PerformanceObserverで読み込みを検知できる様子でした。

  11. Shadow DOM外のimgiframeで参照した場合も同様です。