作为前端,一直以来都知道HTTP劫持与XSS跨站脚本(Cross-site scripting)、CSRF跨站请求伪造(Cross-site request forgery)。但是一直都没有深入研究过,前些日子同事的分享会偶然提及,我也对这一块很感兴趣,便深入研究了一番。
最近用 JavaScript 写了一个组件,可以在前端层面防御部分 HTTP 劫持与 XSS。
当然,防御这些劫持最好的
方法
快递客服问题件处理详细方法山木方法pdf计算方法pdf华与华方法下载八字理论方法下载
还是从后端入手,前端能做的实在太少。而且由于源码的暴露,攻击者很容易绕过我们的防御手段。但是这不代
表
关于同志近三年现实表现材料材料类招标技术评分表图表与交易pdf视力表打印pdf用图表说话 pdf
我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。
已上传到 Github – httphijack.js ,欢迎感兴趣看看顺手点个 star ,本文示例代码,防范方法在组件源码中皆可找到。
接下来进入正文。
HTTP劫持、DNS劫持与XSS
先简单讲讲什么是 HTTP 劫持与 DNS 劫持。
HTTP劫持
什么是HTTP劫持呢,大多数情况是运营商HTTP劫持,当我们使用HTTP请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心
设计
领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计
的网络数据报文,让客户端(通常是浏览器)展示“错误”的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容,大家应该都有遇到过。
DNS劫持
DNS劫持就是通过劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。
DNS 劫持就更过分了,简单说就是我们请求的是 直接被重定向了 ,本文不会过多讨论这种情况。
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 时,可以重定向父级页面:
JavaScript
1
2
3
4
5
6
if (self != top) {
// 我们的正常页面
var url = location.href;
// 父级页面重定向
top.location = url;
}
使用白名单放行正常 iframe 嵌套
当然很多时候,也许运营需要,我们的页面会被以各种方式推广,也有可能是正常业务需要被嵌套在 iframe 中,这个时候我们需要一个白名单或者黑名单,当我们的页面被嵌套在 iframe 中且父级页面域名存在白名单中,则不做重定向操作。
上面也说了,使用 top.location.href 是没办法拿到父级页面的 URL 的,这时候,需要使用document.referrer。
通过 document.referrer 可以拿到跨域 iframe 父页面的URL。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 建立白名单
var whiteList = [
];
if (self != top) {
var
// 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
parentUrl = document.referrer,
length = whiteList.length,
i = 0;
for(; i
分析
定性数据统计分析pdf销售业绩分析模板建筑结构震害分析销售进度分析表京东商城竞争战略分析
这些 URL ,不断增强我们的防护手段,这个后文会提及。
内联事件及内联脚本拦截
在 XSS 中,其实可以注入脚本的方式非常的多,尤其是 HTML5 出来之后,一不留神,许多的新标签都可以用于注入可执行脚本。
列出一些比较常见的注入方式:
1.
2.
3.
4.
5.
除去一些未列出来的非常少见生僻的注入方式,大部分都是 javascript:... 及内联事件 on*。
我们假设注入已经发生,那么有没有办法拦截这些内联事件与内联脚本的执行呢?
对于上面列出的 (1) (5) ,这种需要用户点击或者执行某种事件之后才执行的脚本,我们是有办法进行防御的。
浏览器事件模型
这里说能够拦截,涉及到了事件模型相关的原理。
我们都知道,标准浏览器事件模型存在三个阶段:
l 捕获阶段
l 目标阶段
l 冒泡阶段
对于一个这样
的 a 标签而言,真正触发元素 alert(222) 是处于点击事件的目标阶段。
点击上面的 click me ,先弹出 111 ,后弹出 222。
那么,我们只需要在点击事件模型的捕获阶段对标签内 javascript:... 的内容建立关键字黑名单,进行过滤审查,就可以做到我们想要的拦截效果。
对于 on* 类内联事件也是同理,只是对于这类事件太多,我们没办法手动枚举,可以利用代码自动枚举,完成对内联事件及内联脚本的拦截。
以拦截 a 标签内的 href="javascript:... 为例,我们可以这样写:
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 建立关键词黑名单
var keywordBlackList = [
'xss',
'BAIDU_SSP__wrapper',
'BAIDU_DSPUI_FLOWBAR'
];
document.addEventListener('click', function(e) {
var code = "";
// 扫描
的脚本
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 跨站脚本的精髓不在于“跨站”,在于“脚本”。
通常而言,攻击者或者运营商会向页面中注入一个 是页面加载一开始就存在的静态脚本(查看页面结构),我们使用 MutationObserver 可以在脚本加载之后,执行之前这个时间段对其内容做正则匹配,发现恶意代码则 removeChild() 掉,使之无法执行。
使用白名单对 src 进行匹配过滤
上面的代码中,我们判断一个js脚本是否是恶意的,用的是这一句:
JavaScript
1
if (/xss/i.test(node.src)) {}
当然实际当中,注入恶意代码者不会那么傻,把名字改成 XSS 。所以,我们很有必要使用白名单进行过滤和建立一个拦截上报系统。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 建立白名单
var whiteList = [
/**
* [白名单匹配]
* @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 拦截静态脚本,除了静态脚本,与之对应的就是动态生成的脚本。
JavaScript
1
2
3
4
5
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = '
document.getElementsByTagName('body')[0].appendChild(script);
要拦截这类动态生成的脚本,且拦截时机要在它插入 DOM 树中,执行之前,本来是可以监听 Mutation Events 中的 DOMNodeInserted 事件的。
Mutation Events 与 DOMNodeInserted
打开 MDN ,第一句就是:
该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。
虽然不能用,也可以了解一下:
JavaScript
1
2
3
4
5
6
7
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);
然而可惜的是,使用上面的代码拦截动态生成的脚本,可以拦截到,但是代码也执行了:DOMNodeInserted 顾名思义,可以监听某个 DOM 范围内的结构变化,与 MutationObserver 相比,它的执行时机更早。
但是 DOMNodeInserted 不再建议使用,所以监听动态脚本的任务也要交给 MutationObserver。
可惜的是,在实际实践过程中,使用 MutationObserver 的结果和 DOMNodeInserted 一样,可以监听拦截到动态脚本的生成,但是无法在脚本执行之前,使用 removeChild 将其移除,所以我们还需要想想其他办法。
重写 setAttribute 与 document.write
重写原生 Element.prototype.setAttribute 方法
在动态脚本插入执行前,监听 DOM 树的变化拦截它行不通,脚本仍然会执行。
那么我们需要向上寻找,在脚本插入 DOM 树前的捕获它,那就是创建脚本时这个时机。
假设现在有一个动态脚本是这样创建的:
JavaScript
1
2
3
4
5
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src',);
document.getElementsByTagName('body')[0].appendChild(script);
而重写 Element.prototype.setAttribute 也是可行的:我们发现这里用到了 setAttribute 方法,如果我们能够改写这个原生方法,监听设置 src 属性时的值,通过黑名单或者白名单判断它,就可以判断该标签的合法性了。
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 保存原有接口
var old_setAttribute = Element.prototype.setAttribute;
// 重写 setAttribute 接口
Element.prototype.setAttribute = function(name, value) {
// 匹配到