在傳統觀念,從網頁以新視窗或 IFrame 開啟外部網站連結,只要不隸屬同站台,瀏覽器基於同源政策(Same-Orgin Policy)會禁止外部站台以 JavaScrpit 存取來源網頁,理論上應該是安全的。但事實是,外部網頁仍有機會跨網站搞鬼。

用以下例子展示,假設我有兩個網頁,Home.html 與 ExternalLink.html:

Home.html

<!DOCTYPE html>
<html>

<head>
    <title>來源網站</title>
</head>

<body>
    <a href="ExternalLink.html" target="_blank">
        開啟外部網頁
    </a>
</body>

</html>

ExternalLink.html

<!DOCTYPE html>
<html>
    <bod>
        <div>
            來源網站標題:
            <span id=origPageTitle></span>
        </div>
        <script>
            if (opener) {
                var title = "";
                try {
                    title = opener.document.title;
                }
                catch (ex) {
                    title = "錯誤:" + ex;
                }
                document.getElementById('origPageTitle').innerText  = title;
            }
        </script>
    </bod>
</html>

先將兩個網頁放在同一資料夾,用 VSCode 跑 Live Server 測試。因兩個網頁屬於同站台,ExternalLink.html 可透過 opener.document.title 讀取到 Home.html 的標題,符合預期:

接著改一下 Home.html,將 <a href="ExternalLink.html" target="_blank"> 改成 <a href="http://localhost:5500/ExternalLink.html" target="_blank"> 一樣是同一個 Live Server 站台,但 URL Host 由 127.0.0.1 換成 localhost,對瀏覽器來說已非相同來源,依規定 localhost:5500 的 ExternalLink.html 不能存取 127.0.0.1:5500 Home.html 的內容,瀏覽器很盡責地擋下 opener.document.title 讀取動作:

到目前為止,一切都符合我們的預期。但這裡藏了一個漏洞,雖然 ExternalLink.html 沒法存取 Home.html 的 DOM,但修改 opener.localtion 是被允許的,於是可能出現 - ExternalLink.html 偷偷把 Home.html 換成釣魚網站,掛木馬騙密碼當跳板挖比特幣,能搞的花樣可多了。而這一切只需加一行opener.location.href = 假網頁URL

<!DOCTYPE html>
<html>
    <bod>
        <div>
            來源網站標題:
            <span id=origPageTitle></span>
        </div>
        <script>
            if (opener) {
                var title = "";
                try {
                    title = opener.document.title;
                }
                catch (ex) {
                    title = "錯誤:" + ex;
                }
                document.getElementById('origPageTitle').innerText  = title;
                //把來源網頁換掉
                opener.location.href = "http://localhost:5500/FakeHome.html";
            }
        </script>
    </bod>
</html>

如下圖,開啟 ExternalLink.html 後,Home.html 就被偷換成 FakeHome.html 想騙密碼了:

要防範這種風險很簡單,<a href="http://localhost:5500/ExternalLink.html" target="_blank" rel="noopener"> 加上 rel="noopener" 便能杜絕後患。此時 ExternalLink.html 看到的 opener 將會是 null,就沒法做怪了:

依據 Can I Use,IE11 不支援 noopener,但實測發現 IE 即使沒加 rel="noopener",跨站台時 ExternalLink.html opener 會等於 undefined,不致造成風險。

此外,以 IFrame 內嵌跨站台網頁也存在相同問題,內嵌頁透過 parent.location 或 top.location 也能置換母頁內容(只是畫面會閃一下,較易被發現),IFrame 有個 sandbox Attribute 能提供防護(IE10+ 支援),無法確保內嵌對象安全性,記得加上。

Tips of using rel="noopener" in link to protect your web page from replacement by external link.


Comments

# by 路人甲

看起來很矛盾? 一開始 Home.html 和 ExternalLink.html 是自己的網站內容. 那豈不是要先弄個假的 FakeHome.html 釣魚網站? 才能騙 user 去開啟 FakeHome.html, 才能打開駭客的 FakeExternalLink.html ? 然後 FakeExternalLink.html 才能開 FakeHome.html !!?? 請問要如何把 在 user 開啟 Home.html 狀態下點取 開啟外部網頁, 實際上是跑 FakeExternalLink.html ??

# by Jeffrey

to 路人甲,較會發生問題的情境是 ExternalLink.html 本來是個合法的外部網站,Home.html 去超連結它沒有任何疑義,但後來它被入侵利用來做壞事(或一開始假裝正派,其實是卧底),做資安就是要時時抱著「誰都不能相信」「任何人都有可能是壞人」的零信任思維。(然後搞得人生很累,呵)

Post a comment