Chrome の拡張機能システムでは、かなり厳格なデフォルトのコンテンツ セキュリティ ポリシー(CSP)が適用されます。ポリシーの制限は簡単です。スクリプトはインラインから別の JavaScript ファイルに移動し、インライン イベント ハンドラは addEventListener を使用するように変換し、eval() は無効にする必要があります。
ただし、さまざまなライブラリで、パフォーマンスの最適化や表現の容易さのために eval() や eval のような構造(new Function() など)が使用されていることは認識しています。テンプレート ライブラリは、このスタイルの実装に特に適しています。一部(Angular.js など)は CSP をすぐにサポートしていますが、多くの一般的なフレームワークは、拡張機能の eval なし環境と互換性のあるメカニズムにまだ更新されていません。そのため、この機能のサポートを削除することは、デベロッパーにとって予想以上に問題であることが判明しました。
このドキュメントでは、セキュリティを損なうことなくプロジェクトにこれらのライブラリを含めるための安全なメカニズムとして、サンドボックスを紹介します。
サンドボックスを使用する理由
eval は、実行するコードが拡張機能の高権限環境内のすべてにアクセスできるため、拡張機能内では危険です。ユーザーのセキュリティとプライバシーに深刻な影響を与える可能性のある強力な browser.* API が多数存在します。単純なデータ漏洩は、懸念事項のほんの一部にすぎません。提供されるソリューションは、eval が拡張機能のデータや拡張機能の価値の高い API にアクセスせずにコードを実行できるサンドボックスです。データも API もなくても問題ありません。
これを実現するには、拡張機能パッケージ内の特定の HTML ファイルをサンドボックス化されたものとしてリストします。サンドボックス化されたページが読み込まれるたびに、一意のオリジンに移動し、browser.* 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:
browser.action.onClicked.addListener(() => {
browser.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 は拡張機能ページに返され、拡張機能は後でその 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 分の価値がある内容です。