在沙箱 IFrame 中安全地玩遊戲

Mike West

在現今的網路上建構豐富的體驗時,幾乎一定會涉及嵌入您無法真正控制的元件和內容。第三方小工具可提升使用者參與度,並在整體使用者體驗中扮演重要角色,而使用者產生的內容有時甚至比網站的原生內容更重要。絕對不能放棄,但這兩者都會增加網站發生某些 BadTM 的風險。您嵌入的每個小工具 (每則廣告、每個社群媒體小工具) 都是惡意人士的潛在攻擊媒介:

內容安全政策 (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 推文表單。這就是主要的差異,框架不需要載入任何外掛程式,也不需要瀏覽頂層視窗或任何其他功能。由於不需要這些權限,因此我們要採用沙箱機制的內容,移除這些權限。

沙箱功能會根據白名單運作。我們會先移除所有可能的權限,然後在沙箱設定中新增特定標記,重新開啟個別功能。針對 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.opentarget="_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 區塊中;我們會擷取這些例外狀況,並改為回報友善的錯誤訊息。最後,我們將結果發布回父項視窗。這項操作相當簡單。

父項也是如此,我們會建立使用 textareabutton 執行的小型使用者介面,並透過沙箱的 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>

接下來,我們將連結各項元素以便執行。首先,我們會監聽 iframealert() 對使用者的回應。實際應用程式可能會做一些不那麼惱人的動作:

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" &amp;&amp; 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-originallow-scripts 標記的沙箱,針對相同來源的另一個網頁頁框,則頁框式頁面可能會到達父項,進而完全移除沙箱屬性。

在沙箱中玩遊戲

目前,您可以在多種瀏覽器中使用沙箱功能:Firefox 17 以上版本、IE 10 以上版本,以及撰寫本文時的 Chrome (當然,caniuse 有最新的支援表格)。將 sandbox 屬性套用至所包含的 iframes,即可為所顯示的內容授予特定權限,但僅限內容正常運作所需的權限。這可讓您降低加入第三方內容的風險,而這項風險已超出內容安全政策的範圍。

此外,沙箱是一種強大的技術,可降低聰明的攻擊者利用您程式碼中的漏洞的風險。將單一應用程式分割為一組沙箱服務,每個服務負責一小部分獨立功能,這樣攻擊者就必須不只入侵特定框架的內容,還要入侵其控制器。這項工作難度更高,特別是因為控制器的範圍可以大幅縮減。如果您要求瀏覽器協助處理其他部分,就可以將安全性相關工作用於稽核程式碼。

這並非表示沙箱是解決網際網路安全問題的完整解決方案。這項功能可提供多層防護,除非您能控管使用者的用戶端,否則無法依賴瀏覽器支援所有使用者 (如果您能控管使用者的用戶端,例如企業環境,那麼恭喜您!)。有一天… 不過,目前沙箱是另一層防護機制,可強化防禦機制,但並非完整的防禦機制,您不能單獨依賴這項機制。不過,圖層還是很棒的。建議你使用這個。

延伸閱讀

  • HTML5 應用程式中的權限分離」是一篇有趣的論文,內容是設計一個小型架構,並將其應用於三個現有的 HTML5 應用程式。

  • 沙箱機制搭配其他兩個新的 iframe 屬性 (srcdocseamless) 時,可提供更彈性的運用方式。前者可讓您在填入畫面內容時不必產生 HTTP 要求的額外負擔,後者則可讓樣式流入框架內容。目前這兩者都沒有太好的瀏覽器支援 (Chrome 和 WebKit 的夜間版本),但在未來會是個有趣的組合。舉例來說,您可以透過下列程式碼,對文章執行沙箱註解:

        <iframe sandbox seamless
                srcdoc="<p>This is a user's comment!
                           It can't execute script!
                           Hooray for safety!</p>"></iframe>