打造双剑合璧的 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
提出的几种注入防御方式,进行几个月的实践观察,对广告注入方式大概可以归为两种:完全静态注入、先静态注入后动态修改(创建)。
- 完全静态注入
完全内联 js、css、和 dom,不管是 body
内外,甚是恶心,而且如果是在监控脚本前面注入的,还可以抢先执行,造成防御不起作用。注入的
DOM 也无法清除。 - 先静态注入后动态修改
这种可以分为几种:一种是异步请求接口数据再生成 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 的使用仅有不到两周的经验而已,下面简单说说其优缺点。
缺点:
- CSP
规范也比较累赘,每种类型需要重新配置一份,默认配置不能继承,只能替换,这样会导致整个
header 内容会大大增加。 - 如果业务中有爬虫是抓取了外部图片的话,那么 img
配置要么需要枚举各种域名,要么就信任所有域名。 -
- 移动端 web app 页面,如果有存在 Native 与 web 的通信,那么 iframe
配置只能信任所有域名和协议了。
- 移动端 web app 页面,如果有存在 Native 与 web 的通信,那么 iframe
-
- 一些业务场景导致无法排除内联 script 的情况,所以只能开启
unsafe-inline
- 一些业务场景导致无法排除内联 script 的情况,所以只能开启
-
- 一些库仍在使用 eval,所以避免误伤,也只能开启 unsafe-eval
-
- 由于 iframe 信任所有域名和协议,而 unsafe-inline
开启,使得整个防御效果大大降低
- 由于 iframe 信任所有域名和协议,而 unsafe-inline
优点:
- 通过 connect/script 配置,我们可以控制哪些
外部域名异步请求可以发出,这无疑是大大的福音,即使内联 script
被注入,异步请求仍然发不出,这样一来,除非攻击者把所有的 js
都内联进来,否则注入的功能也运行不了,也无法统计效果如何。 - 通过 reportUri 可以统计到攻击类型和
PV,只不过这个接口的设计不能自定义,上报的内容大部分都是鸡肋。 - object/media
配置可以屏蔽一些外部多媒体的加载,不过这对于视频播放类的业务,也会误伤到。 - 目前 UC 浏览器 Android 版本的客户端和 web 端通信机制都是采用标准的
addJavascriptInterface 注入方式,而 iPhone 版本已将 iframe
通信方式改成 ajax 方式(与页面同域,10.5
全部改造完成),如果是只依赖 UC
浏览器的业务,可以大胆放心使用,如果是需要依赖于第三方平台,建议先开启
reportOnly,将一些本地协议加入白名单,再完全开启防御。
总的来说吧,单靠 CSP
单打独斗显然是不行,即使完全开启所有策略,也不能完成消除注入攻击,但是作为纵深防御体系中的一道封锁防线,价值也是相当有用的。
HTTP劫持
什么是HTTP劫持呢,大多数情况是运营商HTTP劫持,当我们使用HTTP请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心设计的网络数据报文,让客户端(通常是浏览器)展示“错误”的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容,大家应该都有遇到过。
前端防火墙拦截
前端防火墙显然适合作为第一道防线进行设计,可以预先对一些注入的内联 js
代码、script/iframe 源引用进行移除,同时对 script/iframe
源地址修改做监控移除。
基本设计逻辑大概如下:
详细的实现逻辑,参考zjcqoo 的《XSS 前端防火墙》系列文章。
缺点:
- 如果是在监控脚本执行前,注入的脚本已经执行,显然后知后觉无法起防御作用了。
- 一些 DOM 的注入显然无能为力。
优点:
- 可以针对 iframe 做一些自定义的过滤规则,防止对本地通信误伤。
- 可以收集到一些注入行为数据进行分析。
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 收藏
评论
XSS跨站脚本
XSS指的是攻击者利用漏洞,向
Web
页面中注入恶意代码,当用户浏览该页之时,注入的代码会被执行,从而达到攻击的特殊目的。
关于这些攻击如何生成,攻击者如何注入恶意代码到页面中本文不做讨论,只要知道如
HTTP 劫持 和 XSS
最终都是恶意代码在客户端,通常也就是用户浏览器端执行,本文将讨论的就是假设注入已经存在,如何利用
Javascript 进行行之有效的前端防护。
页面被嵌入 iframe 中,重定向 iframe
先来说说我们的页面被嵌入了
iframe
的情况。也就是,网络运营商为了尽可能地减少植入广告对原有网站页面的影响,通常会通过把原有网站页面放置到一个和原页面相同大小的
iframe 里面去,那么就可以通过这个 iframe
来隔离广告代码对原有页面的影响。
这种情况还比较好处理,我们只需要知道我们的页面是否被嵌套在
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
出来之后,一不留神,许多的新标签都可以用于注入可执行脚本。
列出一些比较常见的注入方式:
<a href="javascript:alert(1)" ></a>
<iframe src="javascript:alert(1)" />
<img src='x' onerror="alert(1)" />
<video src='x' onerror="alert(1)" ></video>
<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)
点击图中这几个按钮,可以看到如下:
这里我们用到了黑名单匹配,下文还会细说。
静态脚本拦截
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)
<script type="text/javascript" src="./xss/a.js"></script>
是页面加载一开始就存在的静态脚本(查看页面结构),我们使用
MutationObserver
可以在脚本加载之后,执行之前这个时间段对其内容做正则匹配,发现恶意代码则 removeChild()
掉,使之无法执行。