前端安全实战:JavaScript防御HTTP劫持与XSS攻击详解

作者:安全边界

时间:2026-10-24

浏览:276

本文深入解析前端安全中的HTTP劫持与XSS攻击防护。针对运营商HTTP劫持,通过检测iframe嵌套并重定向父页面解决;针对XSS,利用MutationObserver监控DOM变化拦截静态与动态脚本,重写setAttribute与document.write阻断注入,并锁死apply/call防止绕过。结合白名单过滤与上报系统,构建完整的前端防御体系。

文章目录

HTTP劫持、DNS劫持与XSS

首先简要介绍HTTP劫持与DNS劫持的基本概念。

什么是HTTP劫持

HTTP劫持通常由网络运营商实施。当用户通过HTTP协议访问网站时,运营商会正常数据流中插入精心构造的网络数据包,导致客户端(主要是浏览器)展示非预期的“错误”数据,例如弹窗、宣传广告或直接显示其他网站内容。这种情况在上网过程中较为常见。

什么是DNS劫持

DNS劫持是指攻击者通过控制DNS服务器,获取特定域名的解析记录控制权,进而修改该域名的解析结果。这会导致对该域名的访问被重定向到攻击者指定的IP地址,使得用户无法访问原网站或访问到伪造的网址,其目的是窃取资料或破坏正常服务。

相比HTTP劫持,DNS劫持更为严重。简单来说,用户请求访问 http://www.a.com/index.html 时,直接被重定向到了 http://www.b.com/index.html。本文对此类情况不做过多讨论。

XSS跨站脚本攻击

XSS(跨站脚本攻击)是指攻击者利用Web页面的漏洞,向页面中注入恶意代码。当用户浏览该页面时,注入的代码会被执行,从而达到攻击者的特定目的。

本文不讨论攻击者如何生成或注入恶意代码,而是假设恶意代码已经存在于客户端(浏览器)中,重点探讨如何利用JavaScript进行有效的前端防护。

页面被嵌入iframe及重定向处理

首先讨论页面被嵌入iframe的情况。为了减少植入广告对原有网站页面的影响,网络运营商通常会将原网站页面放置在一个与原页面大小相同的iframe中,从而隔离广告代码对原有页面的影响。

HTTP劫持、DNS劫持与XSS

这种情况相对容易处理。我们只需判断当前页面是否被嵌套在iframe中,如果是,则将外层页面重定向到我们的正常页面即可。

如何判断页面是否存在于iframe中?可以通过window.self与window.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中。此时,我们需要建立白名单或黑名单机制。当页面被嵌套且父级页面域名存在于白名单中时,不进行重定向操作。

前文提到,使用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)" 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:...及内联事件on*。

假设注入已经发生,有没有办法拦截这些内联事件与内联脚本的执行呢?

对于上述列出的(1)至(5),这种需要用户点击或执行某种事件后才执行的脚本,我们有办法进行防御。

浏览器事件模型

这里提到的拦截,涉及事件模型相关的原理。

标准浏览器事件模型存在三个阶段:

  • 捕获阶段
  • 目标阶段
  • 冒泡阶段

对于一个这样<a href="javascript:alert(222)" rel="external nofollow" rel="external nofollow" _fcksavedurl="javascript:alert(222)" ></a>的a标签而言,真正触发元素alert(222)是处于点击事件的目标阶段。


<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。

因此,我们只需要在点击事件模型的捕获阶段对标签内javascript:...的内容建立关键字黑名单,进行过滤审查,就可以实现预期的拦截效果。

对于on*类内联事件同理,只是这类事件太多,无法手动枚举,可以利用代码自动枚举,完成对内联事件及内联脚本的拦截。

以拦截a标签内的href="javascript:...为例,我们可以这样写:


// 建立关键词黑名单
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;
}

点击图中这几个按钮,可以看到如下:

HTTP劫持、DNS劫持与XSS

这里用到了黑名单匹配,下文还会细说。

静态脚本拦截

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

通常而言,攻击者或运营商会向页面中注入一个<script>脚本,具体操作都在脚本中实现。这种劫持方式只需注入一次,有改动时不需要每次都重新注入。

我们假定现在页面上被注入了一个<script src="http://attack.com/xss.js">脚本,我们的目标就是拦截这个脚本的执行。

听起来很困难,意思就是在脚本执行前发现这个可疑脚本,并且销毁它,使之不能执行内部代码。

因此,我们需要用到一些高级API,能够在页面加载时对生成的节点进行检测。

MutationObserver

MutationObserver是HTML5新增的API,功能强大,为开发者提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力。

说得玄乎,大概的意思就是能够监测到页面DOM树的变换,并作出反应。

MutationObserver()该构造函数用来实例化一个新的Mutation观察者对象。


MutationObserver(
  function callback
);

这一大段又是啥?意思是MutationObserver在观测时并非发现一个新元素就立即回调,而是将一个时间片段里出现的所有元素,一起传过来。所以在回调中我们需要进行批量处理。而且,其中的callback会在指定的DOM节点(目标节点)发生变化时被调用。在调用时,观察者对象会传给该函数两个参数,第一个参数是个包含了若干个MutationRecord对象的数组,第二个参数则是这个观察者对象本身。

因此,使用MutationObserver,我们可以对页面加载的每个静态脚本文件,进行监控:


// MutationObserver 的不同兼容性写法
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver ||
window.MozMutationObserver;
// 该构造函数用来实例化一个新的 Mutation 观察者对象
// Mutation 观察者对象能监听在某个范围内的 DOM 树变化
var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    // 返回被添加的节点,或者为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('拦截可疑静态脚本:', node.src);
        } catch (e) {}
      }
    }
  });
});
 
// 传入目标节点和观察选项
// 如果 target 为 document 或者 document.documentElement
// 则当前文档中所有的节点添加与删除操作都会被观察到
observer.observe(document, {
  subtree: true,
  childList: true
});

HTTP劫持、DNS劫持与XSS

<script type="text/javascript" src="./xss/a.js"></script>是页面加载一开始就存在的静态脚本(查看页面结构),我们使用MutationObserver可以在脚本加载之后,执行之前这个时间段对其内容做正则匹配,发现恶意代码则removeChild()掉,使之无法执行。

使用白名单对src进行匹配过滤

上面的代码中,我们判断一个js脚本是否是恶意的,用的是这一句:


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

当然实际当中,注入恶意代码者不会那么傻,把名字改成XSS。所以,我们很有必要使用白名单进行过滤和建立一个拦截上报系统。


// 建立白名单
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);
} 

这里我们已经多次提到白名单匹配了,下文还会用到,所以可以这里把它简单封装成一个方法调用。

动态脚本拦截

上面使用MutationObserver拦截静态脚本,除了静态脚本,与之对应的就是动态生成的脚本。


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

要拦截这类动态生成的脚本,且拦截时机要在它插入DOM树中,执行之前,本来是可以监听Mutation Events中的DOMNodeInserted事件的。

Mutation Events与DOMNodeInserted

打开MDN,第一句就是:

该特性已经从Web标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。

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


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('拦截可疑动态脚本:', node);
  }
}, true);

然而可惜的是,使用上面的代码拦截动态生成的脚本,可以拦截到,但是代码也执行了:DOMNodeInserted顾名思义,可以监听某个DOM范围内的结构变化,与MutationObserver相比,它的执行时机更早。

HTTP劫持、DNS劫持与XSS

但是DOMNodeInserted不再建议使用,所以监听动态脚本的任务也要交给MutationObserver。

可惜的是,在实际实践过程中,使用MutationObserver的结果和DOMNodeInserted一样,可以监听拦截到动态脚本的生成,但是无法在脚本执行之前,使用removeChild将其移除,所以我们还需要想想其他办法。

重写setAttribute与document.write

重写原生Element.prototype.setAttribute方法

在动态脚本插入执行前,监听DOM树的变化拦截它行不通,脚本仍然会执行。

那么我们需要向上寻找,在脚本插入DOM树前的捕获它,那就是创建脚本时这个时机。

假设现在有一个动态脚本是这样创建的:


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也是可行的:我们发现这里用到了setAttribute方法,如果我们能够改写这个原生方法,监听设置src属性时的值,通过黑名单或者白名单判断它,就可以判断该标签的合法性了。


// 保存原有接口
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;
    }
  }
   
  // 调用原始接口
  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;
}

HTTP劫持、DNS劫持与XSS

重写Element.prototype.setAttribute,就是首先保存原有接口,然后当有元素调用setAttribute时,检查传入的src是否存在于白名单中,存在则放行,不存在则视为可疑元素,进行上报并不予以执行。最后对放行的元素执行原生的setAttribute,也就是old_setAttribute.apply(this, arguments);。

上述的白名单匹配也可以换成黑名单匹配。

重写嵌套iframe内的Element.prototype.setAttribute

当然,上面的写法如果old_setAttribute = Element.prototype.setAttribute暴露给攻击者的话,直接使用old_setAttribute就可以绕过我们重写的方法了,所以这段代码必须包在一个闭包内。

当然这样也不保险,虽然当前窗口下的Element.prototype.setAttribute已经被重写了。但是还是有手段可以拿到原生的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内的环境和外层window是完全隔离的。

怎么办?我们看到创建iframe用到了createElement,那么是否可以重写原生createElement呢?但是除了createElement还有createElementNS,还有可能是页面上已经存在iframe,所以不合适。

那就在每当新创建一个新iframe时,对setAttribute进行保护重写,这里又有用到MutationObserver:


/**
 * 使用 MutationObserver 对生成的 iframe 页面进行监控,
 * 防止调用内部原生 setAttribute 及 document.write
 * @return {[type]} [description]
 */
function defenseIframe() {
  // 先保护当前页面
  installHook(window);
}
 
/**
 * 实现单个 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;
 
  // 该构造函数用来实例化一个新的 Mutation 观察者对象
  // Mutation 观察者对象能监听在某个范围内的 DOM 树变化
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      // 返回被添加的节点,或者为null.
      var nodes = mutation.addedNodes;
 
      // 逐个遍历
      for (var i = 0; i < nodes.length; i++) {
        var node = nodes[i];
 
        // 给生成的 iframe 里环境也装上重写的钩子
        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方法,参数是一个window,在这个方法里,我们将重写传入的window下的setAttribute,并且安装一个MutationObserver,并对此窗口下未来可能创建的iframe进行监听,如果未来在此window下创建了一个iframe,则对新的iframe也装上installHook方法,以此进行层层保护。

重写document.write

根据上述的方法,我们可以继续挖掘一下,还有什么方法可以重写,以便对页面进行更好的保护。

document.write是一个很不错选择,注入攻击者,通常会使用这个方法,往页面上注入一些弹窗广告。

我们可以重写document.write,使用关键词黑名单对内容进行匹配。

什么比较适合当黑名单的关键字呢?我们可以看看一些广告很多的页面:

HTTP劫持、DNS劫持与XSS

这里在页面最底部嵌入了一个iframe,里面装了广告代码,这里的最外层的id名id="BAIDU_SSP__wrapper_u2444091_0"就很适合成为我们判断是否是恶意代码的一个标志,假设我们已经根据拦截上报收集到了一批黑名单列表:


// 建立正则拦截关键词
var keywordBlackList = [
'xss',
'BAIDU_SSP__wrapper',
'BAIDU_DSPUI_FLOWBAR'
];

接下来我们只需要利用这些关键字,对document.write传入的内容进行正则判断,就能确定是否要拦截document.write这段代码。


// 建立关键词黑名单
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;
    }
 
    // 调用原始接口
    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方法中,就能对当前window及所有生成的iframe环境内的document.write进行重写了。

锁死apply和call

接下来要介绍的这个是锁住原生的Function.prototype.apply和Function.prototype.call方法,锁住的意思就是使之无法被重写。

这里要用到Object.defineProperty,用于锁死apply和call。

Object.defineProperty

Object.defineProperty()方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象。


Object.defineProperty(obj, prop, descriptor)

其中:

  • obj – 需要定义属性的对象
  • prop – 需被定义或修改的属性名
  • descriptor – 需被定义或修改的属性的描述符

我们可以使用如下的代码,让call和apply无法被重写。


// 锁住 call
Object.defineProperty(Function.prototype, 'call', {
  value: Function.prototype.call,
  // 当且仅当仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变
  writable: false,
  // 当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除
  configurable: false,
  enumerable: true
});
// 锁住 apply
Object.defineProperty(Function.prototype, 'apply', {
  value: Function.prototype.apply,
  writable: false,
  configurable: false,
  enumerable: true
}); 

为啥要这样写呢?其实还是与上文的重写setAttribute有关。

虽然我们将原始Element.prototype.setAttribute保存在了一个闭包当中,但是还有奇技淫巧可以把它从闭包中给“偷出来”。

试一下:


(function() {})(
    // 保存原有接口
    var old_setAttribute = Element.prototype.setAttribute;
    // 重写 setAttribute 接口
    Element.prototype.setAttribute = function(name, value) {
        // 具体细节
        if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {}
        // 调用原始接口
        old_setAttribute.apply(this, arguments);
    };
)();
// 重写 apply
Function.prototype.apply = function(){
    console.log(this);
}
// 调用 setAttribute
document.getElementsByTagName('body')[0].setAttribute('data-test','123'); 

猜猜上面一段会输出什么?看看:

HTTP劫持、DNS劫持与XSS

居然返回了原生setAttribute方法!

这是因为我们在重写Element.prototype.setAttribute时最后有old_setAttribute.apply(this, arguments);这一句,使用到了apply方法,所以我们再重写apply,输出this,当调用被重写后的setAttribute就可以从中反向拿到原生的被保存起来的old_setAttribute了。

这样我们上面所做的嵌套iframe重写setAttribute就毫无意义了。

使用上面的Object.defineProperty可以锁死apply和类似用法的call。使之无法被重写,那么也就无法从闭包中将我们的原生接口偷出来。这个时候才算真正意义上的成功重写了我们想重写的属性。

建立拦截上报

防御的手段也有一些了,接下来我们要建立一个上报系统,替换上文中的console.log()日志。

上报系统有什么用呢?因为我们用到了白名单,关键字黑名单,这些数据都需要不断的丰富,靠的就是上报系统,将每次拦截的信息传到服务器,不仅可以让我们程序员第一时间得知攻击的发生,更可以让我们不断收集这类相关信息以便更好的应对。

这里的示例我用nodejs搭一个十分简易的服务器接受http上报请求。

先定义一个上报函数:


/**
 * 自定义上报 -- 替换页面中的 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;
}

假定我们的服务器地址是www.reportServer.com这里,我们运用img.src发送一个http请求到服务器http://www.reportServer.com/report/,每次会带上我们自定义的拦截类型,拦截内容以及上报时间。

用Express搭nodejs服务器并写一个简单的接收路由:


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!');
});

运行服务器,当有上报发生,我们将会接收到如下数据:

HTTP劫持、DNS劫持与XSS

好接下来就是数据入库,分析,添加黑名单,使用nodejs当然拦截发生时发送邮件通知程序员等等,这些就不再做展开。

HTTPS与CSP

最后再简单谈谈HTTPS与CSP。其实防御劫持最好的方法还是从后端入手,前端能做的实在太少。而且由于源码的暴露,攻击者很容易绕过我们的防御手段。

CSP

CSP即是Content Security Policy,翻译为内容安全策略。这个规范与内容安全有关,主要是用来定义页面可以加载哪些资源,减少XSS的发生。

MDN –CSP

HTTPS

能够实施HTTP劫持的根本原因,是HTTP协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则劫持将无法轻易发生。

HTTPS,是HTTP over SSL的意思。SSL协议是Netscape在1995年首次提出的用于解决传输层安全问题的网络协议,其核心是基于公钥密码学理论实现了对服务器身份认证、数据的私密性保护以及对数据完整性的校验等功能。

因为与本文主要内容关联性不大,关于更多CSP和HTTPS的内容可以自行谷歌。

以上就是详解前端安全之JavaScript防http劫持与XSS的详细内容,更多关于前端安全之JavaScript防http劫持与XSS的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章