Chrome 的擴充功能系統會強制執行相當嚴格的預設內容安全政策 (CSP)。政策限制很簡單:指令碼必須移至離線狀態,並放入個別的 JavaScript 檔案;內嵌事件處理常式必須轉換為使用 addEventListener
,且 eval()
會遭到停用。
不過,我們也瞭解,許多程式庫都會使用 eval()
和 eval
類似的結構體 (例如 new Function()
),以便提升效能並簡化表達方式。模板化程式庫特別容易採用這種實作方式。雖然有些框架 (例如 Angular.js) 支援 CSP 的即時支援功能,但許多熱門框架尚未更新為與擴充功能的 eval
無世界相容的機制。因此,對於開發人員而言,移除這項功能的支援比預期更麻煩。
本文件將沙箱做為安全機制,說明如何在專案中納入這些程式庫,同時不犧牲安全性。
為什麼要使用沙箱?
eval
在擴充功能中是危險的,因為執行的程式碼可存取擴充功能高權限環境中的所有內容。我們提供多種功能強大的 chrome.*
API,這些 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
。我們稍後會深入探討 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
插入內容不會造成重大安全風險,因為我們信任在沙箱中顯示的內容。
這個機制可讓您輕鬆建立範本,但當然不限於範本。任何在嚴格內容安全性政策下無法立即運作的程式碼,都可以進行沙箱處理;事實上,將擴充功能的元件沙箱處理,通常很有幫助,因為這樣做會限制程式的每個部分,只允許執行所需的最小權限集合。2012 年 Google I/O 大會的「Writing Secure Web Apps and Chrome Extensions」簡報提供了這些技術實際應用的實例,值得花 56 分鐘的時間觀看。