在沙箱 IFrame 中安全地玩遊戲

Mike West

您幾乎無可避免地要建構在現今網路環境中豐富的體驗,包括嵌入了您無法實際控制的嵌入元件和內容。第三方小工具不僅能提高參與度,也對整體使用者體驗扮演重要角色,而使用者產生的內容有時比網站原生內容更重要。完全無法選擇證據,但這會增加 BadTM 在網站上發生的風險。您嵌入的每個小工具 (每則廣告、每個社群媒體小工具) 都可能是惡意意圖使用者可能遭受攻擊的媒介:

內容安全政策 (CSP) 可讓您將明確受信任的指令碼來源和其他內容加入許可清單,藉此降低這兩種內容類型相關的風險。這確實朝著正確的方向發展,但值得注意的是,大多數 CSP 指令所提供的保護都是二進位:允許資源,或者是不允許的資源。有時候建議您思考:「我不確定自己實際上信任這個來源的內容來源,不過這點真的很不錯!請嵌入瀏覽器,但別讓它中斷我的網站。」

最低權限

基本上,我們正在尋找機制,希望能夠允許我們只嵌入執行工作所需的最低能力等級的內容。如果小工具「需要」不彈出新的視窗,就無法關閉 window.open 的存取權限。如果不需要 Flash,關閉外掛程式支援應該不會造成問題。我們採取最低權限原則會盡力保障您的安全,並封鎖與我們想使用的功能未直接相關的每項功能。因此,我們再也不必擔心某些嵌入內容無法使用不應使用的權限。這只是一開始就無法存取功能。

如要為這類解決方案建立良好的架構,首先,iframe 元素會是第一個步驟。在 iframe 中載入某些不受信任的元件,可讓您測量應用程式與要載入的內容之間的關聯。頁框內容無法存取網頁的 DOM,或您儲存在本機的資料,也無法繪製到頁面上的任意位置;其範圍僅限於頁框的輪廓。不過,分隔並沒有真正穩健的能力。該包含的網頁仍有多種造成困擾或惡意行為的選項:自動播放影片、外掛程式和彈出式視窗是冰山一角。

iframe 元素的 sandbox 屬性提供我們所需的項目,以加強對頁框內容的限制。我們可以指示瀏覽器在低權限環境中載入特定影格的內容,只允許執行所有工作所需的必要功能子集。

小心,但需驗證

Twitter 的「Twitter」按鈕是很好的範例,可進一步安全地透過沙箱將功能嵌入網站。Twitter 可讓您使用下列程式碼透過 iframe 嵌入按鈕

<iframe src="https://platform.twitter.com/widgets/tweet_button.html"
        style="border: 0; width:130px; height:20px;"></iframe>

為了找出可以鎖定的項目,以下將仔細檢視按鈕需要的功能。載入到頁框的 HTML 會從 Twitter 伺服器執行一些 JavaScript,並在使用者點選時產生彈出式視窗,以填入 Tweet 介面。這個介面需要存取 Twitter 的 Cookie,以便將 Tweet 連結至正確的帳戶,並且需要提交 Tweet 表單。這幾乎是如此:頁框不需要載入任何外掛程式,也不需要前往頂層視窗或任何其他位元的功能。由於這個架構並不需要這些權限,因此我們要透過沙箱機制,將影格內容移除。

沙箱作業會以許可清單為基礎。我們會先移除所有可能的權限,然後在沙箱的設定中加入特定標記,重新開啟個別功能。如果是 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 包含空白的沙箱屬性,則框架文件將採用全面沙箱機制,但有下列限制:

  • 不會在頁框式文件中執行 JavaScript。這不僅包含透過指令碼標記明確載入的 JavaScript,還有內嵌事件處理常式和 javascript: 網址,也就是說,系統也會顯示 noscript 標記中的內容,就像使用者自行停用指令碼一樣。
  • 框架文件會載入至不重複的來源,表示所有相同來源檢查都會失敗;不重複的來源絕不會與任何其他來源比對,甚至完全沒有相符。基於其他影響,文件無法存取任何來源 Cookie 或任何其他儲存機制 (DOM 儲存空間、索引資料庫等) 中儲存的資料。
  • 框架文件無法建立新的視窗或對話方塊 (例如透過 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,因為按鈕會在新視窗中開啟 Tweet 表單。
  • allow-forms 為必要元素,因為 Tweet 表單必須提交。
  • allow-same-origin 為必要項目,因為否則將無法存取 twitter.com 的 Cookie,使用者也無法登入並張貼表單。

值得一提的是,套用至影格的沙箱標記也會套用至沙箱中建立的任何視窗或頁框。也就是說,即使表單只存在於影格彈出的視窗中,仍須將 allow-forms 新增至影格的沙箱。

設定 sandbox 屬性後,小工具只會取得需要的權限,而外掛程式、頂端導覽和指標鎖定等功能都會遭到封鎖。我們大幅降低了嵌入小工具的風險,不會造成不良影響。這對每個人而言是一大利多。

權限區隔

對第三方內容執行沙箱機制,以便在低權限環境中執行他們不受信任的程式碼,顯然非常實用。但那自己的程式碼呢?你相信自己,對吧?多虧了沙箱功能呢?

我會思考這個問題:如果您的程式碼不需要外掛程式,為什麼要授予外掛程式存取權?最好是,您從未使用過的權限,最糟的,它是攻擊者可能會走進大門的潛在媒介。每個人的程式碼都含有錯誤,幾乎每個應用程式都容易遭人利用。對自己的程式碼採用沙箱機制,代表即使攻擊者成功入侵應用程式,也無法取得應用程式來源的完整存取權,而他們只能執行應用程式可執行的操作。仍然這樣,但還是不如預期。

您可以進一步降低風險,方法是將應用程式分成多個邏輯元件,並盡可能讓各部分具備最低權限。這項技術在原生程式碼中相當常見:舉例來說,Chrome 會自行破壞成高權限瀏覽器程序,能夠存取本機硬碟,且可能建立網路連線;此外,還有許多低權限轉譯器程序,會大量剖析不受信任的內容。轉譯器不需要觸碰磁碟,瀏覽器會負責提供轉譯網頁所需的所有資訊。即使聰明的駭客找到了轉譯器毀損的方法,她並沒有什麼特別的問題,因為轉譯器無法自行做到很大的關注:所有高權限存取權都必須透過瀏覽器的程序轉送。攻擊者必須找出系統不同環節的幾個漏洞,才能做出任何損害,從而大幅降低成功開源的風險。

安全沙箱 eval()

採用沙箱機制和 postMessage API 時,這個模型的成功方法是直接適用於網頁。應用程式的元件可在沙箱的 iframe 中運作,且父項文件可透過發布訊息和監聽回應來引導兩者之間的通訊。這類結構可確保應用程式任一部分的漏洞都能盡量減少損害。這個範本也有一個優點,那就是您必須建立明確的整合點,這樣您才能確切瞭解在驗證輸入和輸出內容方面。我們來看一個玩具範例

Evalbox 是有趣的應用程式,能擷取字串,並將其評估為 JavaScript。哇,對吧?跟你許久不見的事物一樣當然,這是一種相當危險的應用程式,因為允許任意 JavaScript 執行,就代表來源提供的所有資料都應當抓取。為降低發生 Bad ThingsTM 攻擊的風險,我們會確保程式碼是在沙箱中執行,以便大幅提高安全性。我們會從畫面內部處理程式碼,從影格內容開始:

<!-- 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 屬性,也就是父項視窗。完成後,我們會使用這個 ID 將工作備份的結果傳送給我們。接著,我們會透過將取得的資料傳遞至 eval(),來完成繁重的工作。這項呼叫已納入 try 區塊中,因為在沙箱的 iframe 中禁止的作業經常會產生 DOM 例外狀況;我們將擷取這些例外狀況,並改為回報友善的錯誤訊息。最後,我們會將結果回傳到父項視窗。這非常簡單。

上層也同樣不複雜。我們會建立小型 UI,分別使用 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 中,就如上述內容一樣。高權限父項視窗可做為控制器和調度工具,向每個模組傳送訊息至每個模組中,每個模組的權限可能最低,包括執行工作、監聽結果,以及確保每個模組都只提供所需的資訊。

不過請注意,處理與父項相同的影格內容時,請務必謹慎處理。如果 https://example.com/ 上的某個網頁使用沙箱對同一來源上的另一個網頁框架,且沙箱中同時含有 allow-same-originallow-scripts 標記,則頁框式網頁可以指向上層網頁,然後完全移除沙箱屬性。

在沙箱中玩遊戲

沙箱功能目前已在多種瀏覽器上使用,在撰寫本文當下,可使用 Firefox 17 以上版本、IE10 以上版本和 Chrome (當然,您也可以使用最新的支援表格)。將 sandbox 屬性套用至內含的 iframes 時,您「只能」授予內容正常運作所需的權限,如此一來,您就能降低納入第三方內容的風險,而不超出內容安全政策涵蓋範圍。

此外,沙箱是一項強大的技術,可降低聰明的攻擊者運用您程式碼中漏洞的風險。將單體式應用程式分割為一組採用沙箱機制的服務後,每個服務都負責一小塊獨立功能,攻擊者不僅會入侵特定影格的內容,也會受到控制器的內容影響。這屬於相當困難的工作,尤其是因為控制器可以大幅縮小範圍。如果您要求瀏覽器協助完成其餘步驟,就能用安全性相關工作稽核「該」程式碼。

這並不表示,沙箱是網際網路安全性問題的完整解決方案。這套系統提供深入防禦機制,除非您能控管使用者的用戶端,否則目前您無法仰賴所有使用者的瀏覽器支援 (如果您可以控管使用者的用戶端,例如企業環境,例如 Horay!)。有時... 但現在,採用沙箱機制是強化防禦的另一層機制,並不是您可以完全仰賴的防禦機制。不過,圖層很棒,建議多加利用

其他資訊

  • HTML5 應用程式中的權限分隔」是一篇有趣的論文,是以小型架構的設計,以及其為三個現有的 HTML5 應用程式進行設計。

  • srcdocseamless 這兩個新的 iframe 屬性搭配使用時,沙箱功能可以更靈活。前者可讓您將內容填入頁框中,而不會產生 HTTP 要求的負擔,而後者可讓樣式傳入頁框內容。目前這兩項功能都對瀏覽器支援相當不可思議 (例如 Chrome 和 WebKit 晚上都會支援),但日後也會很有趣。例如,您可以利用以下程式碼,讓沙箱對文章留言:

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