前言 一看到HTTP劫持的,早读君就有兴趣看。本文由@ChokCoco带来的分享,并且会有点长,需点耐心看。
正文从这开始~ 作为前端,一直以来都知道HTTP劫持与XSS跨站脚本(Cross-site scripting)、CSRF跨站请求伪造(Cross-site request forgery)。但是一直都没有深入研究过,前些日子同事的分享会偶然提及,我也对这一块很感兴趣,便深入研究了一番。 最近用 JavaScript 写了一个组件,可以在前端层面防御部分 HTTP 劫持与 XSS。 当然,防御这些劫持最好的方法还是从后端入手,前端能做的实在太少。而且由于源码的暴露,攻击者很容易绕过我们的防御手段。但是这不代表我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。 已上传到 Github – httphijack.js ,欢迎感兴趣看看顺手点个 star ,本文示例代码,防范方法在组件源码中皆可找到。 HTTP劫持、DNS劫持与XSS 先简单讲讲什么是 HTTP 劫持与 DNS 劫持。
IIS7网站监控可以做到提前预防各类网站劫持、并且是免费在线查询、适用于各大站长、政府网站、学校、公司、医院等网站。他可以做到24小时定时监控、同时它可以让你知道网站是否被黑、被入侵、被改标题、被挂黑链、被劫持、被墙及DNS是否被污染等等功能、更是拥有独家检测网站真实的完全打开时间、让你作为站长能清楚的知道自己网站的健康情况!
官方地址:IIS7网站监控
官方图
HTTP劫持 什么是HTTP劫持呢,大多数情况是运营商HTTP劫持,当我们使用HTTP请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心设计的网络数据报文,让客户端(通常是浏览器)展示“错误”的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容,大家应该都有遇到过。 DNS劫持 DNS劫持就是通过劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。 DNS 劫持就更过分了,简单说就是我们请求的是 http://www.a.com/index.html ,直接被重定向了 http://www.b.com/index.html ,本文不会过多讨论这种情况。 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 时,可以重定向父级页面:
使用白名单放行正常 iframe 嵌套
当然很多时候,也许运营需要,我们的页面会被以各种方式推广,也有可能是正常业务需要被嵌套在 iframe 中,这个时候我们需要一个白名单或者黑名单,当我们的页面被嵌套在 iframe 中且父级页面域名存在白名单中,则不做重定向操作。
上面也说了,使用 top.location.href 是没办法拿到父级页面的 URL 的,这时候,需要使用document.referrer。
通过 document.referrer 可以拿到跨域 iframe 父页面的URL。
更改 URL 参数绕过运营商标记
这样就完了吗?没有,我们虽然重定向了父页面,但是在重定向的过程中,既然第一次可以嵌套,那么这一次重定向的过程中页面也许又被 iframe 嵌套了,真尼玛蛋疼。
当然运营商这种劫持通常也是有迹可循,最常规的手段是在页面 URL 中设置一个参数,例如 http://www.example.com/index.html?iframe_hijack_redirected=1 ,其中 iframe_hijack_redirected=1 表示页面已经被劫持过了,就不再嵌套 iframe 了。所以根据这个特性,我们可以改写我们的 URL ,使之看上去已经被劫持了:
当然,如果这个参数一改,防嵌套的代码就失效了。所以我们还需要建立一个上报系统,当发现页面被嵌套时,发送一个拦截上报,即便重定向失败,也可以知道页面嵌入 iframe 中的 URL,根据分析这些 URL ,不断增强我们的防护手段,这个后文会提及。
内联事件及内联脚本拦截
在 XSS 中,其实可以注入脚本的方式非常的多,尤其是 HTML5 出来之后,一不留神,许多的新标签都可以用于注入可执行脚本。
列出一些比较常见的注入方式:
除去一些未列出来的非常少见生僻的注入方式,大部分都是 javascript:… 及内联事件 on*。
我们假设注入已经发生,那么有没有办法拦截这些内联事件与内联脚本的执行呢?
对于上面列出的 (1) (5) ,这种需要用户点击或者执行某种事件之后才执行的脚本,我们是有办法进行防御的。
浏览器事件模型
这里说能够拦截,涉及到了事件模型相关的原理。
我们都知道,标准浏览器事件模型存在三个阶段:
捕获阶段
目标阶段
冒泡阶段
对于一个这样 的 a 标签而言,真正触发元素 alert(222) 是处于点击事件的目标阶段。
点击上面的 click me ,先弹出 111 ,后弹出 222。
那么,我们只需要在点击事件模型的捕获阶段对标签内 javascript:… 的内容建立关键字黑名单,进行过滤审查,就可以做到我们想要的拦截效果。
对于 on* 类内联事件也是同理,只是对于这类事件太多,我们没办法手动枚举,可以利用代码自动枚举,完成对内联事件及内联脚本的拦截。
以拦截 a 标签内的 href="javascript:… 为例,我们可以这样写:
这里我们用到了黑名单匹配,下文还会细说。
静态脚本拦截
XSS 跨站脚本的精髓不在于“跨站”,在于“脚本”。
通常而言,攻击者或者运营商会向页面中注入一个
我们假定现在页面上被注入了一个
听起来很困难啊,什么意思呢。就是在脚本执行前发现这个可疑脚本,并且销毁它使之不能执行内部代码。
所以我们需要用到一些高级 API ,能够在页面加载时对生成的节点进行检测。
MutationObserver
MutationObserver 是 HTML5 新增的 API,功能很强大,给开发者们提供了一种能在某个范围内的 DOM 树发生变化时作出适当反应的能力。
说的很玄乎,大概的意思就是能够监测到页面 DOM 树的变换,并作出反应。
MutationObserver() 该构造函数用来实例化一个新的Mutation观察者对象。
目瞪狗呆,这一大段又是啥?意思就是 MutationObserver 在观测时并非发现一个新元素就立即回调,而是将一个时间片段里出现的所有元素,一起传过来。所以在回调中我们需要进行批量处理。而且,其中的 callback 会在指定的 DOM 节点(目标节点)发生变化时被调用。在调用时,观察者对象会传给该函数两个参数,第一个参数是个包含了若干个 MutationRecord 对象的数组,第二个参数则是这个观察者对象本身。
所以,使用 MutationObserver ,我们可以对页面加载的每个静态脚本文件,进行监控:
是页面加载一开始就存在的静态脚本(查看页面结构),我们使用 MutationObserver 可以在脚本加载之后,执行之前这个时间段对其内容做正则匹配,发现恶意代码则 removeChild() 掉,使之无法执行。但是 DOMNodeInserted 不再建议使用,所以监听动态脚本的任务也要交给 MutationObserver。
可惜的是,在实际实践过程中,使用 MutationObserver 的结果和 DOMNodeInserted 一样,可以监听拦截到动态脚本的生成,但是无法在脚本执行之前,使用 removeChild 将其移除,所以我们还需要想想其他办法。
重写 setAttribute 与 document.write
重写原生 Element.prototype.setAttribute 方法
在动态脚本插入执行前,监听 DOM 树的变化拦截它行不通,脚本仍然会执行。
那么我们需要向上寻找,在脚本插入 DOM 树前的捕获它,那就是创建脚本时这个时机。
假设现在有一个动态脚本是这样创建的:
而重写 Element.prototype.setAttribute 也是可行的:我们发现这里用到了 setAttribute 方法,如果我们能够改写这个原生方法,监听设置 src 属性时的值,通过黑名单或者白名单判断它,就可以判断该标签的合法性了。
重写 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 。
通过这个方法,可以重新拿到原生的 Element.prototype.setAttribute ,因为 iframe 内的环境和外层 window 是完全隔离的。wtf?
怎么办?我们看到创建 iframe 用到了 createElement,那么是否可以重写原生 createElement 呢?但是除了createElement 还有 createElementNS ,还有可能是页面上已经存在 iframe,所以不合适。
那就在每当新创建一个新 iframe 时,对 setAttribute 进行保护重写,这里又有用到 MutationObserver :
我们定义了一个 installHook 方法,参数是一个 window ,在这个方法里,我们将重写传入的 window 下的 setAttribute ,并且安装一个 MutationObserver ,并对此窗口下未来可能创建的 iframe 进行监听,如果未来在此 window 下创建了一个 iframe ,则对新的 iframe 也装上 installHook 方法,以此进行层层保护。
重写 document.write
根据上述的方法,我们可以继续挖掘一下,还有什么方法可以重写,以便对页面进行更好的保护。
document.write 是一个很不错选择,注入攻击者,通常会使用这个方法,往页面上注入一些弹窗广告。
我们可以重写 document.write ,使用关键词黑名单对内容进行匹配。
HTTPS
能够实施 HTTP 劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则劫持将无法轻易发生。
HTTPS,是 HTTP over SSL 的意思。SSL 协议是 Netscape 在 1995 年首次提出的用于解决传输层安全问题的网络协议,其核心是基于公钥密码学理论实现了对服务器身份认证、数据的私密性保护以及对数据完整性的校验等功能。
因为与本文主要内容关联性不大,关于更多 CSP 和 HTTPS 的内容可以自行谷歌。