美文网首页
【前端安全】JavaScript防http劫持与XSS

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

作者: IM魂影 | 来源:发表于2018-04-23 19:28 被阅读0次

    作为前端,一直以来都知道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 来隔离广告代码对原有页面的影响。


    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)

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

    image

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

    静态脚本拦截

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

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

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

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

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

    MutationObserver

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

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

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

    MutationObserver(
      function callback
    );
    

    相关文章

      网友评论

          本文标题:【前端安全】JavaScript防http劫持与XSS

          本文链接:https://www.haomeiwen.com/subject/drgllftx.html