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