在現今的網路上建構豐富的體驗時,幾乎一定會涉及嵌入您無法真正控制的元件和內容。第三方小工具可提升使用者參與度,並在整體使用者體驗中扮演重要角色,而使用者產生的內容有時比網站的原生內容更重要。您無法完全避免這兩種情況,但這兩種情況都會增加網站發生「Something Bad™」的風險。您嵌入的每個小工具 (每則廣告、每個社群媒體小工具) 都是惡意人士的潛在攻擊媒介:
內容安全政策 (CSP) 可讓您將特定信任的指令碼來源和其他內容加入白名單,藉此降低這兩類內容的風險。這是正確方向的重要一步,但值得注意的是,大多數 CSP 指令提供的保護措施是二元式的:資源要嘛允許,要嘛不允許。有時說出「我不知道自己是否真的信任這個內容來源,但它實在太美了!請嵌入,瀏覽器,但不要讓它破壞我的網站。」
最低權限
基本上,我們希望找到一種機制,讓我們只授予嵌入內容執行工作所需的最低級別功能。如果小工具不需要彈出新視窗,移除對 window.open 的存取權也不會造成影響。如果不需要 Flash,關閉外掛程式支援功能應該不會有問題。我們遵循最低權限原則,並封鎖與我們想要使用的功能無關的所有功能,以確保最高安全性。因此,我們不再需要盲目相信某些嵌入式內容不會濫用不該使用的權限。因為這類應用程式根本無法存取該功能。
iframe
元素是建立這類解決方案的良好架構的第一步。在 iframe
中載入部分不受信任的元件,可在應用程式和您要載入的內容之間提供一定程度的區隔。框架內容無法存取網頁的 DOM 或您在本機儲存的資料,也無法在網頁上任意位置繪製;其範圍僅限於框架的輪廓。不過,這種分離方式並非真正可靠。包含的網頁仍有許多惱人或惡意行為的選項,例如自動播放的影片、外掛程式和彈出式視窗。
iframe
元素的 sandbox
屬性可提供所需資訊,以便我們加強對嵌入內容的限制。我們可以指示瀏覽器在低權限環境中載入特定框架的內容,只允許執行所需工作所需的部分功能。
信任但仍須驗證
Twitter 的「Tweet」按鈕就是一個很好的例子,說明如何透過沙箱在網站上更安全地嵌入功能。Twitter 允許您使用以下程式碼透過 iframe 嵌入按鈕:
<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
為了瞭解我們可以鎖定哪些內容,請仔細檢查按鈕所需的功能。載入至框架的 HTML 會執行 Twitter 伺服器中的部分 JavaScript,並產生彈出式視窗,當使用者點選時,彈出式視窗會顯示推文介面。該介面需要存取 Twitter 的 Cookie,才能將推文連結至正確的帳戶,並且需要提交推文表單的功能。這就是主要的差異,框架不需要載入任何外掛程式,也不需要瀏覽頂層視窗或任何其他功能。由於不需要這些權限,請將框架的內容置於沙箱中,藉此移除這些權限。
沙箱會根據白名單運作。我們會先移除所有可能的權限,然後在沙箱設定中加入特定標記,重新啟用個別功能。針對 Twitter 小工具,我們決定啟用 JavaScript、彈出式視窗、表單提交功能,以及 twitter.com 的 Cookie。我們可以透過以下值,在 iframe
中新增 sandbox
屬性:
<iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
src="https://platform.twitter.com/widgets/tweet_button.html"
style="border: 0; width:130px; height:20px;"></iframe>
就是這麼簡單!我們已為框架提供所需的所有功能,瀏覽器會拒絕框架存取我們未透過 sandbox
屬性值明確授予的任何特權。
精細控管功能
我們在上述範例中看到幾個可能的沙箱標記,現在讓我們進一步探討該屬性的內部運作方式。
如果 iframe 的 sandbox 屬性為空白,則會對該 iframe 套用以下限制:
- JavaScript 不會在框架文件中執行。這不僅包括透過 script 標記明確載入的 JavaScript,也包括內嵌事件處理常式和 javascript: 網址。這也表示系統會顯示 noscript 標記中包含的內容,就像使用者自行停用指令碼一樣。
- 框架文件會載入至專屬來源,這表示所有同來源檢查都會失敗;專屬來源與任何其他來源都不相符,甚至連自身也不例外。這會導致其他影響,例如文件無法存取任何來源 Cookie 或其他儲存機制 (DOM 儲存空間、索引 DB 等) 中儲存的資料。
- 框架文件無法建立新的視窗或對話方塊 (例如透過
window.open
或target="_blank"
)。 - 無法提交表單。
- 插件無法載入。
- 框架文件只能導覽自身,而無法導覽其頂層父項。設定
window.top.location
會擲回例外狀況,點選含有target="_top"
的連結也不會有任何影響。 - 系統會封鎖自動觸發的功能,例如自動聚焦的表單元素、自動播放的影片等。
- 無法取得指標鎖定。
- 框架文件所含
iframes
會忽略seamless
屬性。
這項功能相當嚴格,而且將文件載入完全沙箱化的 iframe
中,確實會造成極低的風險。當然,這也無法帶來太多價值:您或許可以為部分靜態內容使用完整的沙箱,但大多數情況下,您都會想放寬一些限制。
除了外掛程式外,您可以為沙箱屬性值新增標記,解除這些限制。沙箱化文件永遠無法執行外掛程式,因為外掛程式是未沙箱化原生程式碼,但其他所有內容都屬於公平遊戲:
allow-forms
可用於提交表單。allow-popups
允許彈出式視窗 (令人驚訝)。allow-pointer-lock
可讓您 (沒想到吧) 鎖定游標。allow-same-origin
可讓文件保留其來源;從https://example.com/
載入的網頁會保留對該來源資料的存取權。allow-scripts
允許執行 JavaScript,也允許功能自動觸發 (因為透過 JavaScript 實作這些功能很簡單)。allow-top-navigation
可讓文件透過導覽頂層視窗,從頁框中脫離。
考量上述因素後,我們可以評估為何在上述 Twitter 範例中使用特定的沙箱標記組合:
allow-scripts
是必要的,因為載入至框架中的網頁會執行一些 JavaScript,以處理使用者互動。allow-popups
是必要元素,因為按鈕會在新視窗中彈出推文表單。allow-forms
為必填項目,因為推文表單必須可提交。allow-same-origin
是必要的,否則無法存取 twitter.com 的 Cookie,使用者也無法登入來發布表單。
請注意,套用至框架的沙箱標記也會套用至在沙箱中建立的任何視窗或框架。也就是說,即使表單只存在於框架彈出式視窗中,我們也必須將 allow-forms
新增至框架的沙箱。
設定 sandbox
屬性後,小工具只會取得所需的權限,而外掛程式、頂端導覽和指標鎖定等功能仍會遭到封鎖。我們已降低嵌入小工具的風險,且不會造成不良影響。
這對所有相關人士來說都是雙贏局面。
權限分離
為了在權限較低的環境中執行不受信任的程式碼,將第三方內容置於沙箱中,顯然有其好處。那麼您自己的程式碼呢?你相信自己,對吧?那麼,為什麼要擔心沙箱?
我會反過來問這個問題:如果程式碼不需要外掛程式,為何要讓程式碼存取外掛程式?即使您從未使用這項權限,但這也可能成為攻擊者入侵系統的潛在途徑。每個人的程式碼都會有錯誤,而且幾乎每個應用程式都可能遭到某種形式的濫用。將自己的程式碼置入沙箱,表示即使攻擊者成功顛覆您的應用程式,也不會獲得應用程式來源的完整存取權;攻擊者只能執行應用程式可執行的操作。雖然這仍不理想,但至少不至於太糟。
您可以將應用程式分割成邏輯區塊,並將每個區塊都以最少的權限進行沙箱處理,進一步降低風險。這種做法在原生程式碼中非常常見:舉例來說,Chrome 會將自身分割成具有高權限的瀏覽器程序,該程序可存取本機硬碟並建立網路連線,以及許多低權限的轉譯器程序,負責解析不受信任的內容。轉譯器不需要接觸磁碟,瀏覽器會負責提供轉譯網頁所需的所有資訊。即使聰明的駭客找到破壞轉譯器的方法,也無法取得太多資訊,因為轉譯器本身無法執行太多有趣的操作:所有高權限存取權都必須透過瀏覽器的程序進行。攻擊者必須在系統的不同部分找到多個漏洞,才能造成任何損害,因此成功入侵的風險大幅降低。
安全沙箱 eval()
有了沙箱和 postMessage
API,這個模型的成功經驗就能輕鬆套用到網頁上。應用程式的各個部分可位於沙箱化的 iframe
中,而父項文件則可透過發布訊息和聆聽回應,仲介處理這些項目之間的通訊。這類結構可確保應用程式任何部分的漏洞都會造成最小損害。這項做法的好處是,您必須建立明確的整合點,因此您會確切知道需要在哪裡小心驗證輸入和輸出內容。讓我們透過玩具示例,瞭解這項功能的運作方式。
Evalbox 是一款很棒的應用程式,可擷取字串並將其評估為 JavaScript。哇,就是你多年來的夢想。當然,這項應用程式相當危險,因為允許任意 JavaScript 執行,就表示來源提供的任何資料都會遭到竊取。我們會確保程式碼是在沙箱中執行,藉此降低發生「壞事」™的風險,讓程式碼更加安全。我們將從內而外逐步瞭解程式碼,首先從影格內容開始:
<!-- frame.html -->
<!DOCTYPE html>
<html>
<head>
<title>Evalbox's Frame</title>
<script>
window.addEventListener('message', function (e) {
var mainWindow = e.source;
var result = '';
try {
result = eval(e.data);
} catch (e) {
result = 'eval() threw an exception.';
}
mainWindow.postMessage(result, event.origin);
});
</script>
</head>
</html>
在框架內,我們有一個最小化的文件,只要鉤掛 window
物件的 message
事件,即可聆聽來自其父項的訊息。只要父項在 iframe 內容上執行 postMessage,就會觸發這個事件,讓我們存取父項要執行的字串。
在處理常式中,我們會擷取事件的 source
屬性,也就是父項視窗。我們會在完成後使用這個網址,將努力的成果傳送回去。接著,我們會將所提供的資料傳遞至 eval()
,完成繁重的工作。由於沙箱化 iframe
中的禁止作業經常會產生 DOM 例外狀況,因此這個呼叫已納入 try 區塊中;我們會擷取這些例外狀況,並改為回報友善的錯誤訊息。最後,我們將結果發布回父項視窗。這項操作相當簡單。
父項也同樣簡單。我們將建立一個小型 UI,其中包含用於程式碼的 textarea
,以及用於執行的 button
,並透過沙箱化的 iframe
拉入 frame.html
,只允許執行指令碼:
<textarea id='code'></textarea>
<button id='safe'>eval() in a sandboxed frame.</button>
<iframe sandbox='allow-scripts'
id='sandboxed'
src='frame.html'></iframe>
接下來,我們將連結這些元素以便執行。首先,我們會聆聽 iframe
的回應,並將這些回應傳達給使用者。alert()
實際應用程式可能會做一些不那麼惱人的動作:
window.addEventListener('message',
function (e) {
// Sandboxed iframes which lack the 'allow-same-origin'
// header have "null" rather than a valid origin. This means you still
// have to be careful about accepting data via the messaging API you
// create. Check that source, and validate those inputs!
var frame = document.getElementById('sandboxed');
if (e.origin === "null" && e.source === frame.contentWindow)
alert('Result: ' + e.data);
});
接下來,我們會連結事件處理常式,以便在 button
上點擊。使用者點選時,我們會擷取 textarea
的目前內容,並將其傳遞至執行用的影格:
function evaluate() {
var frame = document.getElementById('sandboxed');
var code = document.getElementById('code').value;
// Note that we're sending the message to "*", rather than some specific
// origin. Sandboxed iframes which lack the 'allow-same-origin' header
// don't have an origin which you can target: you'll have to send to any
// origin, which might alow some esoteric attacks. Validate your output!
frame.contentWindow.postMessage(code, '*');
}
document.getElementById('safe').addEventListener('click', evaluate);
就是這麼簡單!我們已建立非常簡單的評估 API,因此可以確保評估的程式碼不會存取 cookie 或 DOM 儲存空間等敏感資訊。同樣地,經過評估的程式碼無法載入外掛程式、彈出新視窗,或執行任何其他惱人或惡意活動。
您也可以將單體應用程式分割成單一用途的元件,以便為自己的程式碼執行相同的操作。每個 API 都可以包裝在簡單的訊息 API 中,就像我們在上述內容中所寫的一樣。高權限父項視窗可充當控制器和調度器,將訊息傳送至特定模組,每個模組都擁有執行工作所需的最低權限,並監聽結果,確保每個模組只取得所需的資訊。
不過,請注意,如果要處理與父項相同來源的框架內容,請務必謹慎處理。如果 https://example.com/
上的網頁使用沙箱將同一來源的另一個網頁框住,且該沙箱同時包含 allow-same-origin 和 allow-scripts 標記,則框住的網頁可以向上到達父項,並完全移除沙箱屬性。
在沙箱中玩遊戲
目前,您可以在多種瀏覽器中使用沙箱功能:Firefox 17 以上版本、IE 10 以上版本,以及撰寫本文時的 Chrome (當然,caniuse 有最新的支援表格)。將 sandbox
屬性套用至所包含的 iframes
,即可為其顯示的內容授予特定權限,但僅限內容正常運作所需的權限。這樣一來,您就能降低加入第三方內容的風險,而這項風險已超出內容安全政策的範圍。
此外,沙箱是一種強大的技術,可降低聰明的攻擊者利用您程式碼中的漏洞的風險。將單一應用程式分割成一系列的沙箱服務,每個服務負責一小部分獨立的功能,這樣一來,攻擊者就必須不只入侵特定框架的內容,還要入侵其控制器。這項工作難度更高,特別是因為控制器的範圍可以大幅縮減。如果您要求瀏覽器協助處理其他部分,就可以將安全性相關工作用於稽核該程式碼。
這並非表示沙箱是解決網際網路安全問題的完整解決方案。這項功能可提供多層防護,除非您能控管使用者的用戶端,否則無法依賴瀏覽器支援所有使用者 (如果您能控管使用者的用戶端,例如企業環境,那麼恭喜您!)。有一天… 不過,目前沙箱是另一層防護機制,可強化防禦機制,但並非完整的防禦機制,您不能單獨依賴這項機制。不過,圖層還是很棒的。建議你使用這個。
延伸閱讀
「HTML5 應用程式中的權限分離」是一篇有趣的論文,內容是設計一個小型架構,並將其應用於三個現有的 HTML5 應用程式。
沙箱機制搭配其他兩個新的 iframe 屬性 (
srcdoc
和seamless
) 時,可提供更彈性的使用體驗。前者可讓您在填入畫面內容時不必支付 HTTP 要求的額外負擔,後者則可讓樣式流入框架內容。目前這兩者都沒有太好的瀏覽器支援 (Chrome 和 WebKit 的夜間版本),但在未來會是個有趣的組合。舉例來說,您可以透過以下程式碼將文章的留言置入沙箱:<iframe sandbox seamless srcdoc="<p>This is a user's comment! It can't execute script! Hooray for safety!</p>"></iframe>