Chrome の拡張機能システムは、かなり厳しいデフォルトのコンテンツ セキュリティ ポリシー(CSP)を適用しています。ポリシーによる制限は単純で、スクリプトを個別の JavaScript ファイルから別の JavaScript ファイルに移動する必要があります。また、addEventListener
を使用するようにインライン イベント ハンドラを変換し、eval()
を無効にする必要があります。
ただし、さまざまなライブラリが、パフォーマンスの最適化と表現の容易さのために、new Function()
などの eval()
および eval
に似た構造を使用していることを認識しています。テンプレート化ライブラリは、このスタイルの実装が特に発生しがちです。一部の(Angular.js など)は CSP をすぐにサポートしていますが、一般的なフレームワークの多くは拡張機能の eval
レス環境と互換性のあるメカニズムにまだ更新されていません。したがって、この機能に対するサポートの削除は、デベロッパーにとって予想以上に問題となることがわかっています。
このドキュメントでは、セキュリティを損なうことなくこれらのライブラリをプロジェクトに組み込むための安全なメカニズムとして、サンドボックス化を紹介します。
サンドボックスを選ぶ理由
eval
は、実行されるコードは拡張機能の高権限環境内のすべてにアクセスできるため、拡張機能内では危険です。ユーザーのセキュリティとプライバシーに深刻な影響を与える可能性がある強力な chrome.*
API が数多く用意されています。データの引き出しの被害は最小限で済みます。そこで提供されているソリューションは、eval
が拡張機能のデータまたは拡張機能の高価値 API にアクセスせずにコードを実行可能なサンドボックスです。データも API もなく、問題ありません。
これは、拡張機能パッケージ内の特定の HTML ファイルをサンドボックス化してリストすることで実現できます。サンドボックス化されたページが読み込まれると、そのページは一意の生成元に移動され、chrome.*
API へのアクセスが拒否されます。このサンドボックス化されたページを iframe
を介して拡張機能に読み込む場合、メッセージを渡し、なんらかの方法でそれらのメッセージを処理して、結果が返されるまで待ちます。このシンプルなメッセージ メカニズムにより、eval
に基づくコードを拡張機能のワークフローに安全に組み込むために必要なものがすべて揃っています。
サンドボックスを作成して使用する
コードを直接確認したい場合は、サンドボックス化のサンプル拡張機能をご利用ください。これは、Handlebars テンプレート ライブラリの上に構築された小さなメッセージ API の実例であり、作業に必要なものがすべて揃っています。さらに詳しく知りたい方のために こちらのサンプルを一緒に見てみましょう
マニフェスト内のファイルを一覧表示する
サンドボックス内で実行する各ファイルは、sandbox
プロパティを追加して拡張機能のマニフェストにリストする必要があります。これは重要なステップであり、忘れがちなため、サンドボックス化されたファイルがマニフェストに含まれていることを再度確認してください。このサンプルでは、「sandbox.html」という名前のファイルをサンドボックス化しています。マニフェスト エントリは次のようになります。
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
サンドボックス化されたファイルを読み込む
サンドボックス化されたファイルで何か興味深い処理を行うには、拡張機能のコードで処理できるコンテキストでファイルを読み込む必要があります。この例では、sandbox.html は iframe
を介して拡張機能ページに読み込まれています。ページの javaScript ファイルには、ページ上で iframe
を見つけ、その contentWindow
で postMessage()
を呼び出すことで、ブラウザ アクションがクリックされるたびにメッセージをサンドボックスに送信するコードが含まれています。メッセージは、context
、templateName
、command
の 3 つのプロパティを含むオブジェクトです。context
と command
については後ほど説明します。
service-worker.js:
chrome.action.onClicked.addListener(() => {
chrome.tabs.create({
url: 'mainpage.html'
});
console.log('Opened a tab with a sandboxed page!');
});
extension-page.js:
let counter = 0;
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('reset').addEventListener('click', function () {
counter = 0;
document.querySelector('#result').innerHTML = '';
});
document.getElementById('sendMessage').addEventListener('click', function () {
counter++;
let message = {
command: 'render',
templateName: 'sample-template-' + counter,
context: { counter: counter }
};
document.getElementById('theFrame').contentWindow.postMessage(message, '*');
});
危険な行為
sandbox.html
が読み込まれると、Handlebars ライブラリが読み込まれ、Handlebars が提案する方法でインライン テンプレートを作成およびコンパイルされます。
extension-page.html:
<!DOCTYPE html>
<html>
<head>
<script src="mainpage.js"></script>
<link href="styles/main.css" rel="stylesheet" />
</head>
<body>
<div id="buttons">
<button id="sendMessage">Click me</button>
<button id="reset">Reset counter</button>
</div>
<div id="result"></div>
<iframe id="theFrame" src="sandbox.html" style="display: none"></iframe>
</body>
</html>
sandbox.html:
<script id="sample-template-1" type="text/x-handlebars-template">
<div class='entry'>
<h1>Hello</h1>
<p>This is a Handlebar template compiled inside a hidden sandboxed
iframe.</p>
<p>The counter parameter from postMessage() (outer frame) is:
</p>
</div>
</script>
<script id="sample-template-2" type="text/x-handlebars-template">
<div class='entry'>
<h1>Welcome back</h1>
<p>This is another Handlebar template compiled inside a hidden sandboxed
iframe.</p>
<p>The counter parameter from postMessage() (outer frame) is:
</p>
</div>
</script>
これは失敗に終わりません。Handlebars.compile
が new Function
を使用しても、想定どおりの動作となり、コンパイルされたテンプレートは templates['hello']
に格納されます。
結果を返す
このテンプレートを使用するには、拡張機能ページからコマンドを受け入れるメッセージ リスナーを設定します。渡された command
を使用して、必要な処理を決定します(単なるレンダリング以上の作業を行うことも考えられます。テンプレートの作成、なんらかの方法で管理など)で、context
はレンダリングのためにテンプレートに直接渡されます。レンダリングされた HTML は拡張機能ページに戻され、拡張機能は後でそれを使用して有用な処理を実行できます。
<script>
const templatesElements = document.querySelectorAll(
"script[type='text/x-handlebars-template']"
);
let templates = {},
source,
name;
// precompile all templates in this page
for (let i = 0; i < templatesElements.length; i++) {
source = templatesElements[i].innerHTML;
name = templatesElements[i].id;
templates[name] = Handlebars.compile(source);
}
// Set up message event handler:
window.addEventListener('message', function (event) {
const command = event.data.command;
const template = templates[event.data.templateName];
let result = 'invalid request';
// if we don't know the templateName requested, return an error message
if (template) {
switch (command) {
case 'render':
result = template(event.data.context);
break;
// you could even do dynamic compilation, by accepting a command
// to compile a new template instead of using static ones, for example:
// case 'new':
// template = Handlebars.compile(event.data.templateSource);
// result = template(event.data.context);
// break;
}
} else {
result = 'Unknown template: ' + event.data.templateName;
}
event.source.postMessage({ result: result }, event.origin);
});
</script>
拡張機能ページに戻ると、このメッセージが表示され、渡された html
データを使用して興味深い処理を行います。この例では、単に通知によって出力しますが、この HTML を拡張機能の UI の一部として安全に使用することもできます。innerHTML
を使用して挿入しても、サンドボックス内でレンダリングされたコンテンツを信頼しているため、セキュリティ上の大きなリスクはありません。
このメカニズムにより、テンプレート化が簡単になりますが、テンプレートの作成に限ったものではありません。厳格なコンテンツ セキュリティ ポリシーに基づき、最初から機能しないコードはすべてサンドボックス化できます。実際、正しく実行される拡張機能のコンポーネントをサンドボックス化して、プログラムの各部分を適切な実行に必要な最小限の権限セットに制限することが有用な場合がよくあります。Google I/O 2012 で発表された「Writing Secure Web Apps and Chrome Extensions」プレゼンテーションでは、これらの手法の実例をいくつか紹介しています。所要時間は 56 分です。