亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

詳解前端安全之JavaScript防http劫持與XSS

 更新時間:2021年05月24日 11:18:11   作者:ChokCoco  
作為前端,一直以來都知道HTTP劫持與XSS跨站腳本、CSRF跨站請求偽造。防御這些劫持最好的方法是從后端入手,前端能做的太少。而且由于源碼的暴露,攻擊者很容易繞過防御手段。但這不代表我們?nèi)チ私膺@塊的相關(guān)知識是沒意義的,本文的許多方法,用在其他方面也是大有作用。

HTTP劫持、DNS劫持與XSS

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

HTTP劫持

什么是HTTP劫持呢,大多數(shù)情況是運營商HTTP劫持,當(dāng)我們使用HTTP請求請求一個網(wǎng)站頁面的時候,網(wǎng)絡(luò)運營商會在正常的數(shù)據(jù)流中插入精心設(shè)計的網(wǎng)絡(luò)數(shù)據(jù)報文,讓客戶端(通常是瀏覽器)展示“錯誤”的數(shù)據(jù),通常是一些彈窗,宣傳性廣告或者直接顯示某網(wǎng)站的內(nèi)容,大家應(yīng)該都有遇到過。

DNS劫持

DNS 劫持就是通過劫持了 DNS 服務(wù)器,通過某些手段取得某域名的解析記錄控制權(quán),進而修改此域名的解析結(jié)果,導(dǎo)致對該域名的訪問由原IP地址轉(zhuǎn)入到修改后的指定IP,其結(jié)果就是對特定的網(wǎng)址不能訪問或訪問的是假網(wǎng)址,從而實現(xiàn)竊取資料或者破壞原有正常服務(wù)的目的。

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

XSS跨站腳本

XSS指的是攻擊者利用漏洞,向 Web 頁面中注入惡意代碼,當(dāng)用戶瀏覽該頁之時,注入的代碼會被執(zhí)行,從而達(dá)到攻擊的特殊目的。

關(guān)于這些攻擊如何生成,攻擊者如何注入惡意代碼到頁面中本文不做討論,只要知道如 HTTP 劫持 和 XSS 最終都是惡意代碼在客戶端,通常也就是用戶瀏覽器端執(zhí)行,本文將討論的就是假設(shè)注入已經(jīng)存在,如何利用 Javascript 進行行之有效的前端防護。

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

先來說說我們的頁面被嵌入了 iframe 的情況。也就是,網(wǎng)絡(luò)運營商為了盡可能地減少植入廣告對原有網(wǎng)站頁面的影響,通常會通過把原有網(wǎng)站頁面放置到一個和原頁面相同大小的 iframe 里面去,那么就可以通過這個 iframe 來隔離廣告代碼對原有頁面的影響。

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

那么有沒有方法知道我們的頁面當(dāng)前存在于 iframe 中呢?有的,就是window.self與window.top。

window.self

返回一個指向當(dāng)前 window 對象的引用。

window.top

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

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

兩個屬性分別可以又簡寫為self與top,所以當(dāng)發(fā)現(xiàn)我們的頁面被嵌套在 iframe 時,可以重定向父級頁面:

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

使用白名單放行正常 iframe 嵌套

當(dāng)然很多時候,也許運營需要,我們的頁面會被以各種方式推廣,也有可能是正常業(yè)務(wù)需要被嵌套在 iframe 中,這個時候我們需要一個白名單或者黑名單,當(dāng)我們的頁面被嵌套在 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 參數(shù)繞過運營商標(biāo)記

這樣就完了嗎?沒有,我們雖然重定向了父頁面,但是在重定向的過程中,既然第一次可以嵌套,那么這一次重定向的過程中頁面也許又被 iframe 嵌套了,真尼瑪?shù)疤邸?/p>

當(dāng)然運營商這種劫持通常也是有跡可循,最常規(guī)的手段是在頁面 URL 中設(shè)置一個參數(shù),例如 http://www.example.com/index.html?iframe_hijack_redirected=1,其中iframe_hijack_redirected=1表示頁面已經(jīng)被劫持過了,就不再嵌套 iframe 了。所以根據(jù)這個特性,我們可以改寫我們的 URL ,使之看上去已經(jīng)被劫持了:

var flag = 'iframe_hijack_redirected';
// 當(dāng)前頁面存在于一個 iframe 中
// 此處需要建立一個白名單匹配規(guī)則,白名單默認(rèn)放行
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) {}
}

當(dāng)然,如果這個參數(shù)一改,防嵌套的代碼就失效了。所以我們還需要建立一個上報系統(tǒng),當(dāng)發(fā)現(xiàn)頁面被嵌套時,發(fā)送一個攔截上報,即便重定向失敗,也可以知道頁面嵌入 iframe 中的 URL,根據(jù)分析這些 URL ,不斷增強我們的防護手段,這個后文會提及。

內(nèi)聯(lián)事件及內(nèi)聯(lián)腳本攔截

在 XSS 中,其實可以注入腳本的方式非常的多,尤其是 HTML5 出來之后,一不留神,許多的新標(biāo)簽都可以用于注入可執(zhí)行腳本。

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

1.<a href="javascript:alert(1)" rel="external nofollow" _fcksavedurl="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 onmouseover="alert(2)" ><div>

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

我們假設(shè)注入已經(jīng)發(fā)生,那么有沒有辦法攔截這些內(nèi)聯(lián)事件與內(nèi)聯(lián)腳本的執(zhí)行呢?

對于上面列出的 (1) (5) ,這種需要用戶點擊或者執(zhí)行某種事件之后才執(zhí)行的腳本,我們是有辦法進行防御的。

瀏覽器事件模型

這里說能夠攔截,涉及到了事件模型相關(guān)的原理。

我們都知道,標(biāo)準(zhǔn)瀏覽器事件模型存在三個階段:

  • 捕獲階段
  • 目標(biāo)階段
  • 冒泡階段

對于一個這樣<a href="javascript:alert(222)" rel="external nofollow" rel="external nofollow" _fcksavedurl="javascript:alert(222)" ></a>的 a 標(biāo)簽而言,真正觸發(fā)元素alert(222)是處于點擊事件的目標(biāo)階段。

<a href="javascript:alert(222)" rel="external nofollow"  rel="external nofollow" >Click Me</a>

<script>
  document.addEventListener('click', function(e) {
    alert(111);
  }, true);
</script>

點擊上面的click me,先彈出 111 ,后彈出 222。

那么,我們只需要在點擊事件模型的捕獲階段對標(biāo)簽內(nèi)javascript:...的內(nèi)容建立關(guān)鍵字黑名單,進行過濾審查,就可以做到我們想要的攔截效果。

對于 on* 類內(nèi)聯(lián)事件也是同理,只是對于這類事件太多,我們沒辦法手動枚舉,可以利用代碼自動枚舉,完成對內(nèi)聯(lián)事件及內(nèi)聯(lián)腳本的攔截。

以攔截 a 標(biāo)簽內(nèi)的href="javascript:...為例,我們可以這樣寫:

// 建立關(guān)鍵詞黑名單
var keywordBlackList = [
  'xss',
  'BAIDU_SSP__wrapper',
  'BAIDU_DSPUI_FLOWBAR'
];
   
document.addEventListener('click', function(e) {
  var code = "";
 
  // 掃描 <a href="javascript:" rel="external nofollow" > 的腳本
  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;
}

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

這里我們用到了黑名單匹配,下文還會細(xì)說。

靜態(tài)腳本攔截

XSS 跨站腳本的精髓不在于“跨站”,在于“腳本”。

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

我們假定現(xiàn)在頁面上被注入了一個<script src="http://attack.com/xss.js">腳本,我們的目標(biāo)就是攔截這個腳本的執(zhí)行。

聽起來很困難啊,什么意思呢。就是在腳本執(zhí)行前發(fā)現(xiàn)這個可疑腳本,并且銷毀它使之不能執(zhí)行內(nèi)部代碼。

所以我們需要用到一些高級 API ,能夠在頁面加載時對生成的節(jié)點進行檢測。

MutationObserver

MutationObserver 是 HTML5 新增的 API,功能很強大,給開發(fā)者們提供了一種能在某個范圍內(nèi)的 DOM 樹發(fā)生變化時作出適當(dāng)反應(yīng)的能力。

說的很玄乎,大概的意思就是能夠監(jiān)測到頁面 DOM 樹的變換,并作出反應(yīng)。

MutationObserver()該構(gòu)造函數(shù)用來實例化一個新的Mutation觀察者對象。

MutationObserver(
  function callback
);

目瞪狗呆,這一大段又是啥?意思就是 MutationObserver 在觀測時并非發(fā)現(xiàn)一個新元素就立即回調(diào),而是將一個時間片段里出現(xiàn)的所有元素,一起傳過來。所以在回調(diào)中我們需要進行批量處理。而且,其中的callback會在指定的 DOM 節(jié)點(目標(biāo)節(jié)點)發(fā)生變化時被調(diào)用。在調(diào)用時,觀察者對象會傳給該函數(shù)兩個參數(shù),第一個參數(shù)是個包含了若干個 MutationRecord 對象的數(shù)組,第二個參數(shù)則是這個觀察者對象本身。

所以,使用 MutationObserver ,我們可以對頁面加載的每個靜態(tài)腳本文件,進行監(jiān)控:

// MutationObserver 的不同兼容性寫法
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver ||
window.MozMutationObserver;
// 該構(gòu)造函數(shù)用來實例化一個新的 Mutation 觀察者對象
// Mutation 觀察者對象能監(jiān)聽在某個范圍內(nèi)的 DOM 樹變化
var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    // 返回被添加的節(jié)點,或者為null.
    var nodes = mutation.addedNodes;
 
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      if (/xss/i.test(node.src))) {
        try {
          node.parentNode.removeChild(node);
          console.log('攔截可疑靜態(tài)腳本:', node.src);
        } catch (e) {}
      }
    }
  });
});
 
// 傳入目標(biāo)節(jié)點和觀察選項
// 如果 target 為 document 或者 document.documentElement
// 則當(dāng)前文檔中所有的節(jié)點添加與刪除操作都會被觀察到
observer.observe(document, {
  subtree: true,
  childList: true
});

<script type="text/javascript" src="./xss/a.js"></script>是頁面加載一開始就存在的靜態(tài)腳本(查看頁面結(jié)構(gòu)),我們使用 MutationObserver 可以在腳本加載之后,執(zhí)行之前這個時間段對其內(nèi)容做正則匹配,發(fā)現(xiàn)惡意代碼則removeChild()掉,使之無法執(zhí)行。

使用白名單對 src 進行匹配過濾

上面的代碼中,我們判斷一個js腳本是否是惡意的,用的是這一句:

if (/xss/i.test(node.src)) {}

當(dāng)然實際當(dāng)中,注入惡意代碼者不會那么傻,把名字改成 XSS 。所以,我們很有必要使用白名單進行過濾和建立一個攔截上報系統(tǒng)。

// 建立白名單
var whiteList = [
  'www.aaa.com',
  'res.bbb.com'
];
 
/**
 * [白名單匹配]
 * @param  {[Array]} whileList [白名單]
 * @param  {[String]} value    [需要驗證的字符串]
 * @return {[Boolean]}         [false -- 驗證不通過,true -- 驗證通過]
 */
function whileListMatch(whileList, value) {
  var length = whileList.length,
    i = 0;
 
  for (; i < length; i++) {
    // 建立白名單正則
    var reg = new RegExp(whiteList[i], 'i');
 
    // 存在白名單中,放行
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}
 
// 只放行白名單
if (!whileListMatch(blackList, node.src)) {
  node.parentNode.removeChild(node);
} 

這里我們已經(jīng)多次提到白名單匹配了,下文還會用到,所以可以這里把它簡單封裝成一個方法調(diào)用。

動態(tài)腳本攔截

上面使用 MutationObserver 攔截靜態(tài)腳本,除了靜態(tài)腳本,與之對應(yīng)的就是動態(tài)生成的腳本。

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://www.example.com/xss/b.js';
 
document.getElementsByTagName('body')[0].appendChild(script); 

要攔截這類動態(tài)生成的腳本,且攔截時機要在它插入 DOM 樹中,執(zhí)行之前,本來是可以監(jiān)聽Mutation Events中的DOMNodeInserted事件的。

Mutation Events 與 DOMNodeInserted

打開MDN,第一句就是:

該特性已經(jīng)從 Web 標(biāo)準(zhǔn)中刪除,雖然一些瀏覽器目前仍然支持它,但也許會在未來的某個時間停止支持,請盡量不要使用該特性。

雖然不能用,也可以了解一下:

document.addEventListener('DOMNodeInserted', function(e) {
  var node = e.target;
  if (/xss/i.test(node.src) || /xss/i.test(node.innerHTML)) {
    node.parentNode.removeChild(node);
    console.log('攔截可疑動態(tài)腳本:', node);
  }
}, true);

然而可惜的是,使用上面的代碼攔截動態(tài)生成的腳本,可以攔截到,但是代碼也執(zhí)行了:DOMNodeInserted顧名思義,可以監(jiān)聽某個 DOM 范圍內(nèi)的結(jié)構(gòu)變化,與MutationObserver相比,它的執(zhí)行時機更早。

但是DOMNodeInserted不再建議使用,所以監(jiān)聽動態(tài)腳本的任務(wù)也要交給MutationObserver。

可惜的是,在實際實踐過程中,使用MutationObserver的結(jié)果和DOMNodeInserted一樣,可以監(jiān)聽攔截到動態(tài)腳本的生成,但是無法在腳本執(zhí)行之前,使用removeChild將其移除,所以我們還需要想想其他辦法。

重寫 setAttribute 與 document.write

重寫原生 Element.prototype.setAttribute 方法

在動態(tài)腳本插入執(zhí)行前,監(jiān)聽 DOM 樹的變化攔截它行不通,腳本仍然會執(zhí)行。

那么我們需要向上尋找,在腳本插入 DOM 樹前的捕獲它,那就是創(chuàng)建腳本時這個時機。

假設(shè)現(xiàn)在有一個動態(tài)腳本是這樣創(chuàng)建的:

var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', 'http://www.example.com/xss/c.js');
 
document.getElementsByTagName('body')[0].appendChild(script);

而重寫Element.prototype.setAttribute也是可行的:我們發(fā)現(xiàn)這里用到了 setAttribute 方法,如果我們能夠改寫這個原生方法,監(jiān)聽設(shè)置src屬性時的值,通過黑名單或者白名單判斷它,就可以判斷該標(biāo)簽的合法性了。

// 保存原有接口
var old_setAttribute = Element.prototype.setAttribute;
 
// 重寫 setAttribute 接口
Element.prototype.setAttribute = function(name, value) {
 
  // 匹配到 <script src='xxx' > 類型
  if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {
    // 白名單匹配
    if (!whileListMatch(whiteList, value)) {
      console.log('攔截可疑模塊:', value);
      return;
    }
  }
   
  // 調(diào)用原始接口
  old_setAttribute.apply(this, arguments);
};
 
// 建立白名單
var whiteList = [
'www.yy.com',
'res.cont.yy.com'
];
 
/**
 * [白名單匹配]
 * @param  {[Array]} whileList [白名單]
 * @param  {[String]} value    [需要驗證的字符串]
 * @return {[Boolean]}         [false -- 驗證不通過,true -- 驗證通過]
 */
function whileListMatch(whileList, value) {
  var length = whileList.length,
    i = 0;
 
  for (; i < length; i++) {
    // 建立白名單正則
    var reg = new RegExp(whiteList[i], 'i');
 
    // 存在白名單中,放行
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}

重寫Element.prototype.setAttribute,就是首先保存原有接口,然后當(dāng)有元素調(diào)用 setAttribute 時,檢查傳入的 src 是否存在于白名單中,存在則放行,不存在則視為可疑元素,進行上報并不予以執(zhí)行。最后對放行的元素執(zhí)行原生的setAttribute,也就是old_setAttribute.apply(this, arguments);。

上述的白名單匹配也可以換成黑名單匹配。

重寫嵌套 iframe 內(nèi)的 Element.prototype.setAttribute

當(dāng)然,上面的寫法如果old_setAttribute = Element.prototype.setAttribute暴露給攻擊者的話,直接使用old_setAttribute就可以繞過我們重寫的方法了,所以這段代碼必須包在一個閉包內(nèi)。

當(dāng)然這樣也不保險,雖然當(dāng)前窗口下的Element.prototype.setAttribute已經(jīng)被重寫了。但是還是有手段可以拿到原生的Element.prototype.setAttribute,只需要一個新的 iframe 。

var newIframe = document.createElement('iframe');
document.body.appendChild(newIframe);
 
Element.prototype.setAttribute = newIframe.contentWindow.Element.prototype.setAttribute;

通過這個方法,可以重新拿到原生的Element.prototype.setAttribute,因為 iframe 內(nèi)的環(huán)境和外層 window 是完全隔離的。wtf?

怎么辦?我們看到創(chuàng)建 iframe 用到了createElement,那么是否可以重寫原生createElement呢?但是除了createElement還有createElementNS,還有可能是頁面上已經(jīng)存在 iframe,所以不合適。

那就在每當(dāng)新創(chuàng)建一個新 iframe 時,對setAttribute進行保護重寫,這里又有用到MutationObserver:

/**
 * 使用 MutationObserver 對生成的 iframe 頁面進行監(jiān)控,
 * 防止調(diào)用內(nèi)部原生 setAttribute 及 document.write
 * @return {[type]} [description]
 */
function defenseIframe() {
  // 先保護當(dāng)前頁面
  installHook(window);
}
 
/**
 * 實現(xiàn)單個 window 窗口的 setAttribute保護
 * @param  {[BOM]} window [瀏覽器window對象]
 * @return {[type]}       [description]
 */
function installHook(window) {
  // 重寫單個 window 窗口的 setAttribute 屬性
  resetSetAttribute(window);
 
  // MutationObserver 的不同兼容性寫法
  var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
 
  // 該構(gòu)造函數(shù)用來實例化一個新的 Mutation 觀察者對象
  // Mutation 觀察者對象能監(jiān)聽在某個范圍內(nèi)的 DOM 樹變化
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      // 返回被添加的節(jié)點,或者為null.
      var nodes = mutation.addedNodes;
 
      // 逐個遍歷
      for (var i = 0; i < nodes.length; i++) {
        var node = nodes[i];
 
        // 給生成的 iframe 里環(huán)境也裝上重寫的鉤子
        if (node.tagName == 'IFRAME') {
          installHook(node.contentWindow);
        }
      }
    });
  });
 
  observer.observe(document, {
    subtree: true,
    childList: true
  });
}
 
/**
 * 重寫單個 window 窗口的 setAttribute 屬性
 * @param  {[BOM]} window [瀏覽器window對象]
 * @return {[type]} [description]
 */
function resetSetAttribute(window) {
  // 保存原有接口
  var old_setAttribute = window.Element.prototype.setAttribute;
 
  // 重寫 setAttribute 接口
  window.Element.prototype.setAttribute = function(name, value) {
    ...
  };
} 

我們定義了一個installHook方法,參數(shù)是一個window,在這個方法里,我們將重寫傳入的window下的 setAttribute ,并且安裝一個MutationObserver,并對此窗口下未來可能創(chuàng)建的iframe進行監(jiān)聽,如果未來在此window下創(chuàng)建了一個 iframe ,則對新的iframe也裝上installHook方法,以此進行層層保護。

重寫 document.write

根據(jù)上述的方法,我們可以繼續(xù)挖掘一下,還有什么方法可以重寫,以便對頁面進行更好的保護。

document.write是一個很不錯選擇,注入攻擊者,通常會使用這個方法,往頁面上注入一些彈窗廣告。

我們可以重寫document.write,使用關(guān)鍵詞黑名單對內(nèi)容進行匹配。

什么比較適合當(dāng)黑名單的關(guān)鍵字呢?我們可以看看一些廣告很多的頁面:

這里在頁面最底部嵌入了一個 iframe ,里面裝了廣告代碼,這里的最外層的 id 名id="BAIDU_SSP__wrapper_u2444091_0"就很適合成為我們判斷是否是惡意代碼的一個標(biāo)志,假設(shè)我們已經(jīng)根據(jù)攔截上報收集到了一批黑名單列表:

// 建立正則攔截關(guān)鍵詞
var keywordBlackList = [
'xss',
'BAIDU_SSP__wrapper',
'BAIDU_DSPUI_FLOWBAR'
];

接下來我們只需要利用這些關(guān)鍵字,對document.write傳入的內(nèi)容進行正則判斷,就能確定是否要攔截document.write這段代碼?!?/p>

// 建立關(guān)鍵詞黑名單
var keywordBlackList = [
  'xss',
  'BAIDU_SSP__wrapper',
  'BAIDU_DSPUI_FLOWBAR'
];
 
/**
 * 重寫單個 window 窗口的 document.write 屬性
 * @param  {[BOM]} window [瀏覽器window對象]
 * @return {[type]}       [description]
 */
function resetDocumentWrite(window) {
  var old_write = window.document.write;
 
  window.document.write = function(string) {
    if (blackListMatch(keywordBlackList, string)) {
      console.log('攔截可疑模塊:', string);
      return;
    }
 
    // 調(diào)用原始接口
    old_write.apply(document, arguments);
  }
}
 
/**
 * [黑名單匹配]
 * @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;
} 

我們可以把resetDocumentWrite放入上文的installHook方法中,就能對當(dāng)前 window 及所有生成的 iframe 環(huán)境內(nèi)的document.write進行重寫了。

鎖死 apply 和 call

接下來要介紹的這個是鎖住原生的 Function.prototype.apply 和 Function.prototype.call 方法,鎖住的意思就是使之無法被重寫。

這里要用到Object.defineProperty,用于鎖死 apply 和 call。

Object.defineProperty

Object.defineProperty() 方法直接在一個對象上定義一個新屬性,或者修改一個已經(jīng)存在的屬性, 并返回這個對象。

Object.defineProperty(obj, prop, descriptor)

其中:

  • obj – 需要定義屬性的對象
  • prop – 需被定義或修改的屬性名
  • descriptor – 需被定義或修改的屬性的描述符

我們可以使用如下的代碼,讓 call 和 apply 無法被重寫。

// 鎖住 call
Object.defineProperty(Function.prototype, 'call', {
  value: Function.prototype.call,
  // 當(dāng)且僅當(dāng)僅當(dāng)該屬性的 writable 為 true 時,該屬性才能被賦值運算符改變
  writable: false,
  // 當(dāng)且僅當(dāng)該屬性的 configurable 為 true 時,該屬性才能夠被改變,也能夠被刪除
  configurable: false,
  enumerable: true
});
// 鎖住 apply
Object.defineProperty(Function.prototype, 'apply', {
  value: Function.prototype.apply,
  writable: false,
  configurable: false,
  enumerable: true
}); 

為啥要這樣寫呢?其實還是與上文的重寫 setAttribute有關(guān)。

雖然我們將原始 Element.prototype.setAttribute 保存在了一個閉包當(dāng)中,但是還有奇技淫巧可以把它從閉包中給“偷出來”。

試一下:

(function() {})(
    // 保存原有接口
    var old_setAttribute = Element.prototype.setAttribute;
    // 重寫 setAttribute 接口
    Element.prototype.setAttribute = function(name, value) {
        // 具體細(xì)節(jié)
        if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {}
        // 調(diào)用原始接口
        old_setAttribute.apply(this, arguments);
    };
)();
// 重寫 apply
Function.prototype.apply = function(){
    console.log(this);
}
// 調(diào)用 setAttribute
document.getElementsByTagName('body')[0].setAttribute('data-test','123'); 

猜猜上面一段會輸出什么?看看:

居然返回了原生 setAttribute 方法!

這是因為我們在重寫Element.prototype.setAttribute時最后有old_setAttribute.apply(this, arguments);這一句,使用到了 apply 方法,所以我們再重寫apply,輸出this,當(dāng)調(diào)用被重寫后的 setAttribute 就可以從中反向拿到原生的被保存起來的old_setAttribute了。

這樣我們上面所做的嵌套 iframe 重寫 setAttribute 就毫無意義了。

使用上面的Object.defineProperty可以鎖死 apply 和 類似用法的 call 。使之無法被重寫,那么也就無法從閉包中將我們的原生接口偷出來。這個時候才算真正意義上的成功重寫了我們想重寫的屬性。

建立攔截上報

防御的手段也有一些了,接下來我們要建立一個上報系統(tǒng),替換上文中的 console.log() 日志。

上報系統(tǒng)有什么用呢?因為我們用到了白名單,關(guān)鍵字黑名單,這些數(shù)據(jù)都需要不斷的豐富,靠的就是上報系統(tǒng),將每次攔截的信息傳到服務(wù)器,不僅可以讓我們程序員第一時間得知攻擊的發(fā)生,更可以讓我們不斷收集這類相關(guān)信息以便更好的應(yīng)對。

這里的示例我用nodejs搭一個十分簡易的服務(wù)器接受 http 上報請求。

先定義一個上報函數(shù):

/**
 * 自定義上報 -- 替換頁面中的 console.log()
 * @param  {[String]} name  [攔截類型]
 * @param  {[String]} value [攔截值]
 */
function hijackReport(name, value) {
  var img = document.createElement('img'),
    hijackName = name,
    hijackValue = value.toString(),
    curDate = new Date().getTime();
 
  // 上報
  img.src = 'http://www.reportServer.com/report/?msg=' + hijackName + '&value=' + hijackValue + '&time=' + curDate;
}

假定我們的服務(wù)器地址是www.reportServer.com這里,我們運用img.src發(fā)送一個 http 請求到服務(wù)器http://www.reportServer.com/report/,每次會帶上我們自定義的攔截類型,攔截內(nèi)容以及上報時間。

用 Express 搭 nodejs 服務(wù)器并寫一個簡單的接收路由:

var express = require('express');
var app = express();
 
app.get('/report/', function(req, res) {
    var queryMsg = req.query.msg,
        queryValue = req.query.value,
        queryTime = new Date(parseInt(req.query.time));
 
    if (queryMsg) {
        console.log('攔截類型:' + queryMsg);
    }
 
    if (queryValue) {
        console.log('攔截值:' + queryValue);
    }
 
    if (queryTime) {
        console.log('攔截時間:' + req.query.time);
    }
});
 
app.listen(3002, function() {
    console.log('HttpHijack Server listening on port 3002!');
});

運行服務(wù)器,當(dāng)有上報發(fā)生,我們將會接收到如下數(shù)據(jù):

好接下來就是數(shù)據(jù)入庫,分析,添加黑名單,使用nodejs當(dāng)然攔截發(fā)生時發(fā)送郵件通知程序員等等,這些就不再做展開。

HTTPS 與 CSP

最后再簡單談?wù)?HTTPS 與 CSP。其實防御劫持最好的方法還是從后端入手,前端能做的實在太少。而且由于源碼的暴露,攻擊者很容易繞過我們的防御手段。

CSP

CSP 即是 Content Security Policy,翻譯為內(nèi)容安全策略。這個規(guī)范與內(nèi)容安全有關(guān),主要是用來定義頁面可以加載哪些資源,減少 XSS 的發(fā)生。

MDN –CSP

HTTPS

能夠?qū)嵤?HTTP 劫持的根本原因,是 HTTP 協(xié)議沒有辦法對通信對方的身份進行校驗以及對數(shù)據(jù)完整性進行校驗。如果能解決這個問題,則劫持將無法輕易發(fā)生。

HTTPS,是 HTTP over SSL 的意思。SSL 協(xié)議是 Netscape 在 1995 年首次提出的用于解決傳輸層安全問題的網(wǎng)絡(luò)協(xié)議,其核心是基于公鑰密碼學(xué)理論實現(xiàn)了對服務(wù)器身份認(rèn)證、數(shù)據(jù)的私密性保護以及對數(shù)據(jù)完整性的校驗等功能。

因為與本文主要內(nèi)容關(guān)聯(lián)性不大,關(guān)于更多 CSP 和 HTTPS 的內(nèi)容可以自行谷歌。

以上就是詳解前端安全之JavaScript防http劫持與XSS的詳細(xì)內(nèi)容,更多關(guān)于前端安全之JavaScript防http劫持與XSS的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論