【前端安全】JavaScript防http劫持與XSS

weixin_33716557發表於2018-04-23

作為前端,一直以來都知道HTTP劫持XSS跨站指令碼(Cross-site scripting)、CSRF跨站請求偽造(Cross-site request forgery)。但是一直都沒有深入研究過,前些日子同事的分享會偶然提及,我也對這一塊很感興趣,便深入研究了一番。

最近用 JavaScript 寫了一個元件,可以在前端層面防禦部分 HTTP 劫持與 XSS。

當然,防禦這些劫持最好的方法還是從後端入手,前端能做的實在太少。而且由於原始碼的暴露,攻擊者很容易繞過我們的防禦手段。但是這不代表我們去了解這塊的相關知識是沒意義的,本文的許多方法,用在其他方面也是大有作用。

已上傳到 Github – httphijack.js ,歡迎感興趣看看順手點個 star ,本文示例程式碼,防範方法在元件原始碼中皆可找到。

接下來進入正文。

HTTP劫持、DNS劫持與XSS

先簡單講講什麼是 HTTP 劫持與 DNS 劫持。

HTTP劫持

什麼是HTTP劫持呢,大多數情況是運營商HTTP劫持,當我們使用HTTP請求請求一個網站頁面的時候,網路運營商會在正常的資料流中插入精心設計的網路資料包文,讓客戶端(通常是瀏覽器)展示“錯誤”的資料,通常是一些彈窗,宣傳性廣告或者直接顯示某網站的內容,大家應該都有遇到過。

DNS劫持

DNS 劫持就是通過劫持了 DNS 伺服器,通過某些手段取得某域名的解析記錄控制權,進而修改此域名的解析結果,導致對該域名的訪問由原IP地址轉入到修改後的指定IP,其結果就是對特定的網址不能訪問或訪問的是假網址,從而實現竊取資料或者破壞原有正常服務的目的。

DNS 劫持比之 HTTP 劫持 更加過分,簡單說就是我們請求的是 http://www.a.com/index.html ,直接被重定向了 http://www.b.com/index.html ,本文不會過多討論這種情況。

XSS跨站指令碼

XSS指的是攻擊者利用漏洞,向 Web 頁面中注入惡意程式碼,當使用者瀏覽該頁之時,注入的程式碼會被執行,從而達到攻擊的特殊目的。

關於這些攻擊如何生成,攻擊者如何注入惡意程式碼到頁面中本文不做討論,只要知道如 HTTP 劫持 和 XSS 最終都是惡意程式碼在客戶端,通常也就是使用者瀏覽器端執行,本文將討論的就是假設注入已經存在,如何利用 Javascript 進行行之有效的前端防護。

頁面被嵌入 iframe 中,重定向 iframe

先來說說我們的頁面被嵌入了 iframe 的情況。也就是,網路運營商為了儘可能地減少植入廣告對原有網站頁面的影響,通常會通過把原有網站頁面放置到一個和原頁面相同大小的 iframe 裡面去,那麼就可以通過這個 iframe 來隔離廣告程式碼對原有頁面的影響。


7062626-484494dbeca2dadb.png
image

這種情況還比較好處理,我們只需要知道我們的頁面是否被巢狀在 iframe 中,如果是,則重定向外層頁面到我們的正常頁面即可。

那麼有沒有方法知道我們的頁面當前存在於 iframe 中呢?有的,就是 window.selfwindow.top

window.self

返回一個指向當前 window 物件的引用。

window.top

返回視窗體系中的最頂層視窗的引用。

對於非同源的域名,iframe 子頁面無法通過 parent.location 或者 top.location 拿到具體的頁面地址,但是可以寫入 top.location ,也就是可以控制父頁面的跳轉。

兩個屬性分別可以又簡寫為 self 與 top,所以當發現我們的頁面被巢狀在 iframe 時,可以重定向父級頁面:

if (self != top) {
  // 我們的正常頁面
  var url = location.href;
  // 父級頁面重定向
  top.location = url;
}

使用白名單放行正常 iframe 巢狀

當然很多時候,也許運營需要,我們的頁面會被以各種方式推廣,也有可能是正常業務需要被巢狀在 iframe 中,這個時候我們需要一個白名單或者黑名單,當我們的頁面被巢狀在 iframe 中且父級頁面域名存在白名單中,則不做重定向操作。

上面也說了,使用 top.location.href 是沒辦法拿到父級頁面的 URL 的,這時候,需要使用document.referrer

通過 document.referrer 可以拿到跨域 iframe 父頁面的URL。

// 建立白名單
var whiteList = [
  'www.aaa.com',
  'res.bbb.com'
];
 
if (self != top) {
  var
    // 使用 document.referrer 可以拿到跨域 iframe 父頁面的 URL
    parentUrl = document.referrer,
    length = whiteList.length,
    i = 0;
 
  for(; i<length; i++){
    // 建立白名單正則
    var reg = new RegExp(whiteList[i],'i');
 
    // 存在白名單中,放行
    if(reg.test(parentUrl)){
      return;
    }
  }
 
  // 我們的正常頁面
  var url = location.href;
  // 父級頁面重定向
  top.location = url;
}

更改 URL 引數繞過運營商標記

這樣就完了嗎?沒有,我們雖然重定向了父頁面,但是在重定向的過程中,既然第一次可以巢狀,那麼這一次重定向的過程中頁面也許又被 iframe 巢狀了,真尼瑪蛋疼。

當然運營商這種劫持通常也是有跡可循,最常規的手段是在頁面 URL 中設定一個引數,例如 http://www.example.com/index.html?iframe_hijack_redirected=1 ,其中 iframe_hijack_redirected=1 表示頁面已經被劫持過了,就不再巢狀 iframe 了。所以根據這個特性,我們可以改寫我們的 URL ,使之看上去已經被劫持了:

var flag = 'iframe_hijack_redirected';
// 當前頁面存在於一個 iframe 中
// 此處需要建立一個白名單匹配規則,白名單預設放行
if (self != top) {
  var
    // 使用 document.referrer 可以拿到跨域 iframe 父頁面的 URL
    parentUrl = document.referrer,
    length = whiteList.length,
    i = 0;
 
  for(; i<length; i++){
    // 建立白名單正則
    var reg = new RegExp(whiteList[i],'i');
 
    // 存在白名單中,放行
    if(reg.test(parentUrl)){
      return;
    }
  }
 
  var url = location.href;
  var parts = url.split('#');
  if (location.search) {
    parts[0] += '&' + flag + '=1';
  } else {
    parts[0] += '?' + flag + '=1';
  }
  try {
    console.log('頁面被嵌入iframe中:', url);
    top.location.href = parts.join('#');
  } catch (e) {}
}

當然,如果這個引數一改,防巢狀的程式碼就失效了。所以我們還需要建立一個上報系統,當發現頁面被巢狀時,傳送一個攔截上報,即便重定向失敗,也可以知道頁面嵌入 iframe 中的 URL,根據分析這些 URL ,不斷增強我們的防護手段,這個後文會提及。

內聯事件及內聯指令碼攔截

在 XSS 中,其實可以注入指令碼的方式非常的多,尤其是 HTML5 出來之後,一不留神,許多的新標籤都可以用於注入可執行指令碼。

列出一些比較常見的注入方式:

  1. <a href="javascript:alert(1)" ></a>
  2. <iframe src="javascript:alert(1)" />
  3. <img src='x' onerror="alert(1)" />
  4. <video src='x' onerror="alert(1)" ></video>
  5. <div onclick="alert(1)" onmouseover="alert(2)" ><div>

除去一些未列出來的非常少見生僻的注入方式,大部分都是 javascript:... 及內聯事件 on*

我們假設注入已經發生,那麼有沒有辦法攔截這些內聯事件與內聯指令碼的執行呢?

對於上面列出的 (1) (5) ,這種需要使用者點選或者執行某種事件之後才執行的指令碼,我們是有辦法進行防禦的。

瀏覽器事件模型

這裡說能夠攔截,涉及到了事件模型相關的原理。

我們都知道,標準瀏覽器事件模型存在三個階段:

  • 捕獲階段
  • 目標階段
  • 冒泡階段

對於一個這樣 <a href="javascript:alert(222)" ></a> 的 a 標籤而言,真正觸發元素 alert(222) 是處於點選事件的目標階段。

<iframe src="http://codepen.io/Chokcoco/embed/EyrjkG/?height=265&amp;theme-id=0&amp;default-tab=html,result&amp;embed-version=2" frameborder="no" scrolling="no" width="320" height="265" style="transition: all 0.3s; width: 917px !important;"></iframe>

點選上面的 click me ,先彈出 111 ,後彈出 222。

那麼,我們只需要在點選事件模型的捕獲階段對標籤內 javascript:... 的內容建立關鍵字黑名單,進行過濾審查,就可以做到我們想要的攔截效果。

對於 on* 類內聯事件也是同理,只是對於這類事件太多,我們沒辦法手動列舉,可以利用程式碼自動列舉,完成對內聯事件及內聯指令碼的攔截。

以攔截 a 標籤內的 href="javascript:... 為例,我們可以這樣寫:

// 建立關鍵詞黑名單
var keywordBlackList = [
  'xss',
  'BAIDU_SSP__wrapper',
  'BAIDU_DSPUI_FLOWBAR'
];
   
document.addEventListener('click', function(e) {
  var code = "";
 
  // 掃描 <a href="javascript:"> 的指令碼
  if (elem.tagName == 'A' && elem.protocol == 'javascript:') {
    var code = elem.href.substr(11);
 
    if (blackListMatch(keywordBlackList, code)) {
      // 登出程式碼
      elem.href = 'javascript:void(0)';
      console.log('攔截可疑事件:' + code);
    }
  }
}, true);
 
/**
 * [黑名單匹配]
 * @param  {[Array]} blackList [黑名單]
 * @param  {[String]} value    [需要驗證的字串]
 * @return {[Boolean]}         [false -- 驗證不通過,true -- 驗證通過]
 */
function blackListMatch(blackList, value) {
  var length = blackList.length,
    i = 0;
 
  for (; i < length; i++) {
    // 建立黑名單正則
    var reg = new RegExp(whiteList[i], 'i');
 
    // 存在黑名單中,攔截
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}

可以戳我檢視DEMO。(開啟頁面後開啟控制檯檢視 console.log)

點選圖中這幾個按鈕,可以看到如下:

7062626-78b4cd2fff136708.png
image

這裡我們用到了黑名單匹配,下文還會細說。

靜態指令碼攔截

XSS 跨站指令碼的精髓不在於“跨站”,在於“指令碼”。

通常而言,攻擊者或者運營商會向頁面中注入一個<script>指令碼,具體操作都在指令碼中實現,這種劫持方式只需要注入一次,有改動的話不需要每次都重新注入。

我們假定現在頁面上被注入了一個 <script src="http://attack.com/xss.js"> 指令碼,我們的目標就是攔截這個指令碼的執行。

聽起來很困難啊,什麼意思呢。就是在指令碼執行前發現這個可疑指令碼,並且銷燬它使之不能執行內部程式碼。

所以我們需要用到一些高階 API ,能夠在頁面載入時對生成的節點進行檢測。

MutationObserver

MutationObserver 是 HTML5 新增的 API,功能很強大,給開發者們提供了一種能在某個範圍內的 DOM 樹發生變化時作出適當反應的能力。

說的很玄乎,大概的意思就是能夠監測到頁面 DOM 樹的變換,並作出反應。

MutationObserver() 該建構函式用來例項化一個新的Mutation觀察者物件。

MutationObserver(
  function callback
);

相關文章