Chrome の拡張機能システムでは、かなり厳格なデフォルトのコンテンツ セキュリティ ポリシー(CSP)が適用されます。ポリシーの制限は単純です。スクリプトをオフラインの個別の JavaScript ファイルに移動し、インライン イベント ハンドラを addEventListener
を使用するように変換し、eval()
を無効にする必要があります。
ただし、さまざまなライブラリで、パフォーマンスの最適化と表現の容易さのために、eval()
や eval
のような構造(new Function()
など)が使用されていることを認識しています。テンプレート ライブラリは、特にこのスタイルの実装に適しています。Angular.js など、一部のフレームワークは CSP を標準でサポートしていますが、多くの一般的なフレームワークは、拡張機能の eval
のない世界に対応するメカニズムにまだ更新されていません。そのため、この機能のサポートを終了することは、デベロッパーにとって想定よりも問題が多いことが判明しました。
このドキュメントでは、セキュリティを損なうことなくこれらのライブラリをプロジェクトに含めるための安全なメカニズムとして、サンドボックス化について説明します。
サンドボックスを使用する理由
eval
は拡張機能内で危険です。実行されるコードは、拡張機能の高権限環境内のすべてのものにアクセスできるためです。ユーザーのセキュリティとプライバシーに深刻な影響を与える可能性がある強力な chrome.*
API が多数用意されています。単純なデータ漏洩は、心配の種のほんの一部にすぎません。提案されているソリューションは、eval
が拡張機能のデータや拡張機能の高価値 API にアクセスすることなくコードを実行できるサンドボックスです。データも API も不要です。
これは、拡張機能パッケージ内の特定の HTML ファイルをサンドボックス化されているものとしてリストすることで実現します。サンドボックス化されたページが読み込まれるたびに、そのページは一意のオリジンに移動され、chrome.*
API へのアクセスが拒否されます。このサンドボックス化されたページを iframe
を介して拡張機能に読み込むと、メッセージを渡して、そのメッセージを何らかの方法で処理させ、結果が返されるのを待つことができます。このシンプルなメッセージング メカニズムにより、拡張機能のワークフローに eval
ドリブンのコードを安全に含めるために必要なものがすべて提供されます。
サンドボックスを作成して使用する
すぐにコードを試したい場合は、サンドボックス サンプル拡張機能を入手して開始してください。これは、Handlebars テンプレート ライブラリ上に構築された小さなメッセージング API の動作例であり、開始に必要なものがすべて揃っています。詳しく説明したい場合は、このサンプルを一緒に見てみましょう。
マニフェスト内のファイルを一覧表示する
サンドボックス内で実行する必要がある各ファイルは、sandbox
プロパティを追加して拡張機能マニフェストに登録する必要があります。これは重要なステップであり、忘れがちなので、サンドボックス化されたファイルがマニフェストにリストされていることを再確認してください。このサンプルでは、巧妙に「sandbox.html」という名前のファイルをサンドボックス化しています。マニフェスト エントリは次のようになります。
{
...,
"sandbox": {
"pages": ["sandbox.html"]
},
...
}
サンドボックス化されたファイルを読み込む
サンドボックス化されたファイルで何か面白いことをするには、拡張機能のコードによってアクセスできるコンテキストでファイルを読み込む必要があります。ここでは、iframe
を介して sandbox.html が拡張機能ページに読み込まれています。ページの 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 分間のプレゼンテーションですが、ぜひご覧ください。