作为前端,一直以来都知道
与
- HTTP劫持
(Cross-site scripting)、
- XSS跨站脚本
(Cross-site request forgery)。但是一直都没有深入研究过,前些日子同事的分享会偶然提及,我也对这一块很感兴趣,便深入研究了一番。
- CSRF跨站请求伪造
最近用 JavaScript 写了一个组件,可以在前端层面防御部分 HTTP 劫持与 XSS。
当然,防御这些劫持最好的方法还是从后端入手,前端能做的实在太少。而且由于源码的暴露,攻击者很容易绕过我们的防御手段。但是这不代表我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。
已上传到 Github – ,欢迎感兴趣看看顺手点个 star ,本文示例代码,防范方法在组件源码中皆可找到。
接下来进入正文。
先简单讲讲什么是 HTTP 劫持与 DNS 劫持。
什么是 HTTP 劫持呢,大多数情况是运营商 HTTP 劫持,当我们使用 HTTP 请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心设计的网络数据报文,让客户端(通常是浏览器)展示 "错误" 的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容,大家应该都有遇到过。
DNS 劫持就是通过劫持了 DNS 服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原 IP 地址转入到修改后的指定 IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。
DNS 劫持就更过分了,简单说就是我们请求的是 http://www.a.com/index.html ,直接被重定向了 http://www.b.com/index.html ,本文不会过多讨论这种情况。
XSS 指的是攻击者漏洞,向 web 页面中注入恶意代码,当用户浏览该页之时,注入的代码会被执行,从而达到攻击的特殊目的。
关于这些攻击如何生成,攻击者如何注入恶意代码到页面中本文不做讨论,只要知道如 HTTP 劫持 和 XSS 最终都是恶意代码在客户端,通常也就是用户浏览器端执行,本文将讨论的就是假设注入已经存在,如何利用 Javascript 进行行之有效的前端防护。
先来说说我们的页面被嵌入了 iframe 的情况。也就是,网络运营商为了尽可能地减少植入广告对原有网站页面的影响,通常会通过把原有网站页面放置到一个和原页面相同大小的 iframe 里面去,那么就可以通过这个 iframe 来隔离广告代码对原有页面的影响。
这种情况还比较好处理,我们只需要知道我们的页面是否被嵌套在 iframe 中,如果是,则重定向外层页面到我们的正常页面即可。
那么有没有方法知道我们的页面当前存在于 iframe 中呢?有的,就是
与
- window.self
。
- window.top
返回一个指向当前 window 对象的引用。
返回窗口体系中的最顶层窗口的引用。
对于非同源的域名,iframe 子页面无法通过 parent.location 或者 top.location 拿到具体的页面地址,但是可以写入 top.location ,也就是可以控制父页面的跳转。
两个属性分别可以又简写为
与
- self
,所以当发现我们的页面被嵌套在 iframe 时,可以重定向父级页面:
- top
- 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;
- }
这样就完了吗?没有,我们虽然重定向了父页面,但是在重定向的过程中,既然第一次可以嵌套,那么这一次重定向的过程中页面也许又被 iframe 嵌套了,真尼玛蛋疼。
当然运营商这种劫持通常也是有迹可循,最常规的手段是在页面 URL 中设置一个参数,例如 http://www.example.com/index.html?iframe_hijack_redirected=1 ,其中
表示页面已经被劫持过了,就不再嵌套 iframe 了。所以根据这个特性,我们可以改写我们的 URL ,使之看上去已经被劫持了:
- iframe_hijack_redirected=1
- 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 ,不断增强我们的防护手段,这个后文会提及。
列出一些比较常见的注入方式:
- <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) ,这种需要用户点击或者执行某种事件之后才执行的脚本,我们是有办法进行防御的。
这里说能够拦截,涉及到了
相关的原理。
- 事件模型
我们都知道,标准浏览器事件模型存在三个阶段:
点击上面的
,先弹出 111 ,后弹出 222。
- click me
对于 on* 类内联事件也是同理,只是对于这类事件太多,我们没办法手动枚举,可以利用代码自动枚举,完成对内联事件及内联脚本的拦截。
- // 建立关键词黑名单
- 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;
- }
。(打开页面后打开控制台查看 console.log)
点击图中这几个按钮,可以看到如下:
这里我们用到了黑名单匹配,下文还会细说。
XSS 跨站脚本的精髓不在于 "跨站",在于 "脚本"。
通常而言,攻击者或者运营商会向页面中注入一个
脚本,具体操作都在脚本中实现,这种劫持方式只需要注入一次,有改动的话不需要每次都重新注入。
- "><script>
我们假定现在页面上被注入了一个
脚本,我们的目标就是拦截这个脚本的执行。
- <script src="http://attack.com/xss.js">
听起来很困难啊,什么意思呢。就是在脚本执行前发现这个可疑脚本,并且销毁它使之不能执行内部代码。
所以我们需要用到一些高级 API ,能够在页面加载时对生成的节点进行检测。
MutationObserver 是 HTML5 新增的 API,功能很强大,给开发者们提供了一种能在某个范围内的 DOM 树发生变化时作出适当反应的能力。
说的很玄乎,大概的意思就是能够监测到页面 DOM 树的变换,并作出反应。
该构造函数用来实例化一个新的 Mutation 观察者对象。
- MutationObserver()
- MutationObserver(function callback);
目瞪狗呆,这一大段又是啥?意思就是 MutationObserver 在观测时并非发现一个新元素就立即回调,而是将一个时间片段里出现的所有元素,一起传过来。所以在回调中我们需要进行批量处理。而且,其中的
会在指定的 DOM 节点 (目标节点) 发生变化时被调用。在调用时, 观察者对象会传给该函数两个参数,第一个参数是个包含了若干个 MutationRecord 对象的数组,第二个参数则是这个观察者对象本身。
- callback
所以,使用 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
- });
可以看到如下:。(打开页面后打开控制台查看 console.log)
是页面加载一开始就存在的静态脚本(查看页面结构),我们使用 MutationObserver 可以在脚本加载之后,执行之前这个时间段对其内容做正则匹配,发现恶意代码则
- <script type="text/javascript" src="./xss/a.js"></script>
掉,使之无法执行。
- removeChild()
上面的代码中,我们判断一个 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
打开 ,第一句就是:
该特性已经从 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);
然而可惜的是,使用上面的代码拦截动态生成的脚本,可以拦截到,但是代码也执行了:
顾名思义,可以监听某个 DOM 范围内的结构变化,与
- DOMNodeInserted
相比,它的执行时机更早。
- MutationObserver
但是
不再建议使用,所以监听动态脚本的任务也要交给
- DOMNodeInserted
。
- MutationObserver
可惜的是,在实际实践过程中,使用
的结果和
- MutationObserver
一样,可以监听拦截到动态脚本的生成,但是无法在脚本执行之前,使用
- DOMNodeInserted
将其移除,所以我们还需要想想其他办法。
- removeChild
来源: http://www.cnblogs.com/coco1s/p/5777260.html