【前端安全】JavaScript防http劫持与XSS正规网赌平台

打造双剑合璧的 XSS 前端防火墙

2015/09/30 · HTML5 ·
XSS

原文出处: 林子杰(@Zack__lin)   

作为前端,一直以来都知道HTTP劫持XSS跨站脚本(Cross-site
scripting)、CSRF跨站请求伪造(Cross-site request
forgery)。但是一直都没有深入研究过,前些日子同事的分享会偶然提及,我也对这一块很感兴趣,便深入研究了一番。

前言

深入接触 xss 注入是从排查业务的广告注入开始,以前对 xss
注入片面认为是页面输入的安全校验漏洞导致一系列的问题,通过对 zjcqoo
的《XSS 前端防火墙》系列文章,认识到自己其实对 XSS
注入的认识还真是半桶水。

最近用
JavaScript 写了一个组件,可以在前端层面防御部分 HTTP 劫持与 XSS。

捣蛋的运营商

由于 xss 注入的范围太广,本文仅对网关劫持这一方面的 XSS 注入进行讨论。
这里读者有个小小的疑问,为什么我要选网关劫持进行讨论?因为网关劫持可以大面积范围进行有效控制。

曾经,有这样一道风靡前端的面试题(当然我也现场笔试过):当你在浏览器地址栏输入一个URL后回车,将会发生的事情?其实本文不关心请求发到服务端的具体过程,但是我关心的时,服务端响应输出的文档,可能会在哪些环节被注入广告?手机、路由器网关、网络代理,还有一级运营商网关等等。所以,无论如何,任何网页都得经过运营商网关,而且最调(zui)皮(da)捣(e)蛋(ji)的,就是通过运营商网关。

另外,
也提醒大家,如果手机安装了一些上网加速软件、网络代理软件或设置网络代理
IP,会有安全风险,也包括公共场所/商家的免费 WIFI。

当然,防御这些劫持最好的方法还是从后端入手,前端能做的实在太少。而且由于源码的暴露,攻击者很容易绕过我们的防御手段。但是这不代表我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。

前端防火墙的实践

经过近一段时间通过对 zjcqoo 的《XSS
前端防火墙》
六板斧的反复琢磨理解,基本上防御措施可以归为两大类:一种是从协议上屏蔽,一种是从前端代码层面进行拦截移除。通过
zjcqoo
提出的几种注入防御方式,进行几个月的实践观察,对广告注入方式大概可以归为两种:完全静态注入、先静态注入后动态修改(创建)。

  1. 完全静态注入
    完全内联 js、css、和 dom,不管是 body
    内外,甚是恶心,而且如果是在监控脚本前面注入的,还可以抢先执行,造成防御不起作用。注入的
    DOM 也无法清除。
  2. 先静态注入后动态修改
    这种可以分为几种:一种是异步请求接口数据再生成 DOM 注入,一种是修改
    iframe 源地址进行引入,另外一种是修改 script 源地址,请求执行 js
    再异步获取数据或生成 DOM。

已上传到
Github
– httphijack.js ,欢迎感兴趣看看顺手点个
star ,本文示例代码,防范方法在组件源码中皆可找到。

监控数据观察分析

对 zjcqoo
提出的几种防御方式的实践,前一个月主要是花在优化检测脚本和增加白名单过滤脏数据方面,因为这块事情只能利用业余时间来搞,所以拖的时间有点久。白名单这块的确是比较繁琐,很多人以为分析下已知的域名就
ok 了,其实不然,云龙在这篇 iframe
黑魔法
就提到移动端 Native 与 web
的通信机制,所以在各种 APP 上,会有各种 iframe
的注入,而且是各种五花八门的协议地址,也包括 chrome。

监控拿到的数据很多,但是,由于对整个广告注入黑产行业的不熟悉,所以,有必要借助
google
进行查找研究,发现,运营商大大地狡猾,他们自己只会注入自己业务的广告,如
4G
免费换卡/送流量/送话费,但是商业广告这块蛋糕他们会拱手让人?答案是不可能,他们会勾结其他广告代理公司,利用他们的广告分发平台(运营商被美名为广告系统平台提供商)进行广告投放然后分成…

对于用户投诉,他们一般都是认错,然后对这个用户加白名单,但是他们对其他用户还是继续作恶。对于企业方面的投诉,如果影响到他们的域名,如果你没有确凿的证据,他们就会用各种借口摆脱自己的责任,如用户手机中毒等等,如果你有确凿的证据,还得是他们运营商自己的域名或者
IP,否则他们也无法处理。他们还是一样的借口,用户手机中毒等等。

除非你把运营商的域名或 IP
监控数据列给他看,他才转变态度认错,但是这仅仅也是之前我们提到的流量话费广告,对于第三方广告代理商的广告,还是没法解决,这些第三方广告代理商有广告家、花生米、XX
传媒等等中小型广告商,当然也不排除,有的是“个体户广告商”。

从另一方面来看,由于使用的是古老的 http 协议,这种明文传输的协议,html
内容可以被运营商一清二楚地记录下来,页面关键字、访问时间、地域等用户标签都可以进行采集,说到这,你可能已经明白了一个事(隐私侵犯已经见怪不怪了)——大数据分析+个性化推荐,在
google 一查,运营商还真有部署类似于 iPush
网络广告定向直投这样的系统,而且广告点击率也出奇的高,不排除会定向推送一些偏黄色的图片或游戏。

另外,数据分析中发现一些百度统计的接口请求,也在一些 js
样本中发现百度统计地址,猜测很有可能是这种广告平台利用百度统计系统做数据分析,如定向投放用户
PV 统计,广告效果统计等等。
监控数据分析也扯这么多了,我们还是回来看怎么做防御措施吧!

接下来进入正文。

防御措施介绍

 

全站 HTTPS + HSTS

开启 HTTPS,可以加强数据保密性、完整性、和身份校验,而 HSTS (全称 HTTP
Strict Transport Security)可以保证浏览器在很长时间里都会只用 HTTPS
访问站点,这是该防御方式的优点。但是,缺点和缺陷也不可忽略。

互联网全站HTTPS的时代已经到来 一文已有详细的分析,加密解密的性能损耗在服务端的损耗和网络交互的损耗,但是移动端浏览器和
webview 的兼容性支持却是个问题,比如 Android webview
需要固件4.4以上才支持,iOS safari 8 以上也才支持,而 UC
浏览器目前还不支持。

而目前推动团队所有业务支持 HTTPS 难度也是相当高,部分 302
重定向也有可能存在 SSLStrip,更何况 UC
浏览器还不支持这个协议,很容易通过 SSLStrip
进行劫持利用,虽然运营商大部分情况下不会这么干,但是我还是坚定怀疑他们的节操。由于我国宽带网络的基本国情,短时间指望速度提升基本上不可能的,就算总理一句话,但哪个运营商不想赚钱?所以,业务性能的下降和业务安全,需要进行权衡利弊。

HTTP劫持、DNS劫持与XSS

先简单讲讲什么是
HTTP 劫持与 DNS 劫持。

Content Security Policy(简称 CSP)

CSP
内容安全策略,属于一种浏览器安全策略,以可信白名单作机制,来限制网站中是否可以包含某来源内容。兼容性支持同样是个问题,比如
Android webview 需要固件4.4以上才支持,iOS safari 6 以上支持,幸运的是
UC 浏览器目前支持 1.0
策略版本,具体可以到 CANIUSE 了解。目前对
CSP 的使用仅有不到两周的经验而已,下面简单说说其优缺点。

缺点:

  1. CSP
    规范也比较累赘,每种类型需要重新配置一份,默认配置不能继承,只能替换,这样会导致整个
    header 内容会大大增加。
  2. 如果业务中有爬虫是抓取了外部图片的话,那么 img
    配置要么需要枚举各种域名,要么就信任所有域名。
    1. 移动端 web app 页面,如果有存在 Native 与 web 的通信,那么 iframe
      配置只能信任所有域名和协议了。
    1. 一些业务场景导致无法排除内联 script 的情况,所以只能开启
      unsafe-inline
    1. 一些库仍在使用 eval,所以避免误伤,也只能开启 unsafe-eval
    1. 由于 iframe 信任所有域名和协议,而 unsafe-inline
      开启,使得整个防御效果大大降低

优点:

  1. 通过 connect/script 配置,我们可以控制哪些
    外部域名异步请求可以发出,这无疑是大大的福音,即使内联 script
    被注入,异步请求仍然发不出,这样一来,除非攻击者把所有的 js
    都内联进来,否则注入的功能也运行不了,也无法统计效果如何。
  2. 通过 reportUri 可以统计到攻击类型和
    PV,只不过这个接口的设计不能自定义,上报的内容大部分都是鸡肋。
  3. object/media
    配置可以屏蔽一些外部多媒体的加载,不过这对于视频播放类的业务,也会误伤到。
  4. 目前 UC 浏览器 Android 版本的客户端和 web 端通信机制都是采用标准的
    addJavascriptInterface 注入方式,而 iPhone 版本已将 iframe
    通信方式改成 ajax 方式(与页面同域,10.5
    全部改造完成),如果是只依赖 UC
    浏览器的业务,可以大胆放心使用,如果是需要依赖于第三方平台,建议先开启
    reportOnly,将一些本地协议加入白名单,再完全开启防御。

总的来说吧,单靠 CSP
单打独斗显然是不行,即使完全开启所有策略,也不能完成消除注入攻击,但是作为纵深防御体系中的一道封锁防线,价值也是相当有用的。

HTTP劫持

什么是HTTP劫持呢,大多数情况是运营商HTTP劫持,当我们使用HTTP请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心设计的网络数据报文,让客户端(通常是浏览器)展示“错误”的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容,大家应该都有遇到过。

前端防火墙拦截

前端防火墙显然适合作为第一道防线进行设计,可以预先对一些注入的内联 js
代码、script/iframe 源引用进行移除,同时对 script/iframe
源地址修改做监控移除。
基本设计逻辑大概如下:

正规网赌平台 1

详细的实现逻辑,参考zjcqoo 的《XSS 前端防火墙》系列文章。

缺点:

  1. 如果是在监控脚本执行前,注入的脚本已经执行,显然后知后觉无法起防御作用了。
  2. 一些 DOM 的注入显然无能为力。

优点:

  1. 可以针对 iframe 做一些自定义的过滤规则,防止对本地通信误伤。
  2. 可以收集到一些注入行为数据进行分析。

DNS劫持

DNS
劫持就是通过劫持了 DNS
服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。

DNS
劫持比之 HTTP 劫持
更加过分,简单说就是我们请求的是 http://www.a.com/index.html
,直接被重定向了 http://www.b.com/index.html
,本文不会过多讨论这种情况。

双剑合璧

即使是单纯的 DOM
注入,显然无法满足更高级功能的使用,也会使运营商的广告分发平台效果大打折扣。如果单独其中一种方式进行使用,也只是发挥了一招一式的半成功力,如果是双手互搏,那也可以发挥成倍的功力。

而前端防火墙再加上 CSP
安全策略,双剑合璧,则可以大大降低广告注入带来的负面效果,重则造成广告代码严重瘫痪无法运行:在监控脚本后注入广告脚本,基本上可以被前端防火墙封杀殆尽,即使有漏网之鱼,也会被
CSP 进行追杀,不死也残。

即使在监控脚本运行前注入,通过 CSP content-src
策略,可以拦截白名单域名列表外的接口请求,使得广告代码的异步请求能力被封杀,script-src
策略,也可以封杀脚本外链的一些外部请求,进一步封杀异步脚本引用,frame-src
策略无论先后创建的 iframe,一律照杀。

侥幸者躲过了初一,却躲不过十五,前端防火墙拍马赶到,照样封杀无误,唯一的路径只有注入
DOM 这一方式,别忘了,只要开启 img-src
策略配置,广告代码只剩下文字链。虽然是一个文字链广告,但点击率又能高到哪去呢?

如果你是 node
派系,小弟附上《辟邪剑谱》 helmet 一本,如果你的业务有涉及到
UCBrowser,更有《辟邪剑谱之 UC
版》helmet-csp-uc 。

所谓道高一尺魔高一丈,既然我们有高效的防御措施,相信他们不久也会探索出反防御方式,如此,我们也需要和这帮人斗智斗勇,一直等到
HTTP/2 规范的正式落地。

1 赞 3 收藏
评论

正规网赌平台 2

XSS跨站脚本

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

关于这些攻击如何生成,攻击者如何注入恶意代码到页面中本文不做讨论,只要知道如
HTTP 劫持 和 XSS
最终都是恶意代码在客户端,通常也就是用户浏览器端执行,本文将讨论的就是假设注入已经存在,如何利用
Javascript 进行行之有效的前端防护。

 

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

先来说说我们的页面被嵌入了
iframe
的情况。也就是,网络运营商为了尽可能地减少植入广告对原有网站页面的影响,通常会通过把原有网站页面放置到一个和原页面相同大小的
iframe 里面去,那么就可以通过这个 iframe
来隔离广告代码对原有页面的影响。
正规网赌平台 3

这种情况还比较好处理,我们只需要知道我们的页面是否被嵌套在
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 中,这个时候我们需要一个白名单或者黑名单,当我们的页面被嵌套在
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) 是处于点击事件的目标阶段。

点击上面的 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) 

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

正规网赌平台 4

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

 

静态脚本拦截

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

可以看到如下:可以戳我查看DEMO。(打开页面后打开控制台查看
console.log)

正规网赌平台 5

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

 

相关文章