在 Web 安全领域中,XSS 和 CSRF 是最常见的攻击方式。本文将会简单介绍造成 XSS 的原因,如何防御。
声明:本文的示例仅用于演示相关的攻击原理
XSS
XSS,即 Cross Site Script,中译是跨站脚本攻击;其原本缩写是 CSS,但为了和层叠样式表(Cascading Style Sheet)有所区分,因而在安全领域叫做 XSS。
XSS是一种代码注入攻击,攻击者通过恶意脚本对客户端网页进行篡改,从而在用户浏览客户端页面时获取该用户cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。
XSS的本质:攻击者通过攻击系统存在的XSS漏洞,将恶意代码与正常代码混在一起;浏览器无法分辨哪些脚本是可信的,恶意脚本被执行;由于是直接在用户终端执行,所以恶意脚本可以直接获取用户的信息,并利用这些信息冒充用户向网站发起攻击者定义的请求。
XSS的分类
XSS攻击可以分为3类:反射型(非持久型)、存储型(持久型)、基于DOM。
反射型
反射型 XSS 只是简单地把用户输入的数据 “反射” 给浏览器,这种攻击方式往往需要攻击者诱使用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。
看一个示例,如下是一个静态页:
恶意链接的地址指向了localhost:8088/xxx=xxx
。然后,我再启一个简单的 Node 服务处理恶意链接的请求:
const http = require('http')
function handleRequest(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
res.write('<script>alert("反射型 XSS 攻击")</script>');
res.end();
}
const server = new http.Server()
server.listen(8088,'127.0.0.1')
server.on('request',handleRequest)
当用户点击恶意链接时,页面跳转到攻击者预先准备的页面,会发现在攻击者的页面执行了 js 脚本:
这样就产生了反射型 XSS 攻击。攻击者可以注入任意的恶意脚本进行攻击,可能注入恶作剧脚本,或者注入能获取用户隐私数据(如cookie)的脚本,这取决于攻击者的目的。可能看到这里,你仍然还是存在疑问。示例是直接在接口返回的数据里面插入的恶意脚本,这与实际的场景并不相符。那我们接下来来看看实际场景中攻击者是如何利用XSS漏洞进行的反射型攻击。
再看一个示例,如下是一段php代码:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>XSS原理分析</title>
</head>
<body>
<form action="" method="get">
<input type="text" name="xss_input"><input type="submit">
</form>
<hr>
<?php
$xss = $_GET['xss_input'];
echo '请输入您的payload<br>'.$xss;
?>
</body>
</html>
然后在浏览器中打开会看到如下页面:
我们首先输入123,看看能得到什么结果: 我们输入的字符串被原封不动输出了,这样的话,如果我们将123替换成bird <script>alert('XSS')</script>
也会被原封不动的输出,由于alert()函数的功能是弹出对话框,那么也就是说我们输入上面的语句就会弹框,现在来看看
下面是输入bird <script>alert('XSS')</script>
得到的页面:
果不其然真的弹框了,这就说明这个web页面存在XSS漏洞
再来看看源码:
反射型 XSS 的攻击步骤:
1.攻击者构造出特殊的 URL,其中包含恶意代码。
2.用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器.
3.用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
4.恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
小结:
攻击者通过前端XSS漏洞提交带有恶意脚本的数据成功实现了XSS,但仅仅对此次访问产生了影响,是非持久型攻击。所以攻击者通常会诱导用户去点击此类恶意URL,从而达到他的目的。
存储型
存储型 XSS 会把用户输入的数据 "存储" 在服务器端,当浏览器请求数据时,脚本从服务器上传回并执行。这种 XSS 攻击具有很强的稳定性。
比较常见的一个场景是攻击者在社区或论坛上写下一篇包含恶意 JavaScript 代码的文章或评论,文章或评论发表后,所有访问该文章或评论的用户,都会在他们的浏览器中执行这段恶意的 JavaScript 代码。
举一个示例,先准备一个输入页面:
代码如下:
<input type="text" id="input">
<button id="btn">Submit</button>
<script>
const input = document.getElementById('input');
const btn = document.getElementById('btn');
let val;
input.addEventListener('change', (e) => {
val = e.target.value;
}, false);
btn.addEventListener('click', (e) => {
fetch('http://localhost:8088/save', {
method: 'POST',
body: val
});
}, false);
</script>
启动一个 Node 服务监听 save 请求。为了简化,用一个变量来保存用户的输入:
const http = require('http')
let userInput = '';
function handleRequest(req,res) {
const method = req.method;
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
if (method === 'POST' && req.url === '/save') {
let body = '';
req.on('data', chunk => {
body += chunk;
});
req.on('end', () => {
if (body) {
userInput = body;
}
res.end();
});
} else {
res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
res.write(userInput);
res.end();
}
}
const server = new http.Server();
server.listen(8088, '127.0.0.1');
server.on('request', handleRequest);
当用户点击提交按钮将输入信息提交到服务端时,服务端通过 userInput 变量保存了输入内容。当用户通过 http://localhost:8088/${id}
访问时,服务端会返回与 id 对应的内容(本示例简化了处理)。如果用户输入了恶意脚本内容,则其他用户访问该内容时,恶意脚本就会在浏览器端执行:(偷个懒,别人的图,效果一样)
小结:
攻击者通过XSS漏洞,把带有恶意脚本的代码成功提交给服务器,并且存入数据库。当其他用户再访问这个页面时就会自动触发,属于持久型攻击。
基于DOM
客户端JavaScript可以访问浏览器的DOM文本对象模型是利用的前提,当确认客户端代码中有DOM型XSS漏洞时,并且能诱使(钓鱼)一名用户访问自己构造的URL,就说明可以在受害者的客户端注入恶意脚本。利用步骤和反射型很类似,但是唯一的区别就是,构造的URL参数不用发送到服务器端,可以达到绕过WAF、躲避服务端的检测效果。
为了更方便理解,下面我举几个场景给大家理解。
场景一:跳转
<html>
<head>
<title> DOM-XSS TEST </title>
</head>
<body>
<script>
var hash = location.hash;
if(hash){
var url = hash.substring(1);
location.href = url;
}
</script>
</body>
</html>
正常访问是用#去实现页面跳转,但是因为跳转部分参数可控,可能导致Dom xss。
通过 location.hash
的方式,将参数写在 # 号后,既能让JS读取到该参数,又不让该参数传入到服务器,从而避免了WAF的检测。
变量hash作为可控部分,并带入url中,变量hash控制的是#之后的部分,可以使用伪协议#javascript:alert(1)
。常见的几种伪协议有javascript:
、vbscript:
、data:
等。
场景二:eval
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
eval("var x = '" + location.hash + "'");
</script>
</body>
</html>
场景三:indexOf判断URL参数是否合法
var t = location.search.slice(1) // 变量t取url中?之后的部分
if(t.indexOf("url=") > -1 && t.indexOf("http") > -1){ // 限定传入url中要带有indexOf的关键词
var pos = t.indexOf("url=")+4; // 往后截取
url = t.slice(pos,t.length);
location.href = url // 跳转
}
indexOf会对url进行判断,是否存在关键字http,两个关键字都满足才能执行下面的部分,如果正常请求?url=http://cos.top15.cn
没问题,我们构造javascript:alert(1)
,并不会弹窗,正如我们上段所言,需要满足indexOf带有的关键字,所以只要构造javascript:alert(1)//http
即可完成攻击,有的匹配indexOf("http://cos.top15.cn")
,看似好像没问题,其实构造javascript:alert(1)//http://cos.top15.cn
即可绕过,实际上这种使用indexOf来判断跳转来路域名的方法是不负责任,容易被绕过。
......
需防护场景
- 所有将不可信的数据输出到HTML页面时的场景
<?php
$xss = $_GET['xss_input']; //漏洞示例,原样直出
?>
- 将GET参数值按原值输出到页面中(包括HTTP头、HTML标签、javascript、CSS等处),必须做反射XSS防护。
- 将用户提交的文本内容存储在后台并在前端展示的场景。如用户注册(姓名、产品名、签名、个人简介)、评论、反馈、UCG发表、文件名,必须做存储型XSS防护。
- 若涉及到URL目的跳转的逻辑(例如
location.replace
、location.href
等),或者涉及使用innerHTML、document.write、eval
等敏感函数,必须做DOM型XSS防护。 - 返回JSON的接口务必将
Content-Type
响应头设置为application/json
或text/plain
。 - 涉及对页面内容进行多次编码的处理注意避免由于反编码等操作而导致的mXSS,即将原本无害编码内容又反编码为有害内容而导致的XSS。
- 注意避免使用存在XSS漏洞前端js组件,如小于等于1.11.3的jQuery版本。
防护方案
对输入参数进行类型、字符集、长度的限制
想要“完美”防御XSS,需要每个开发都完全了解XSS的知识,在合适的场景用合适的方案来编码。因此不存在一个函数或一个库可以完美解决问题。所以相对来说,通过限制用户合法输入,可以避免反射、存储、DOM XSS等复杂场景的过滤和转义,最大限度避免恶意脚本内容传入。
示例
- 用户名正则参考:
^[0-9a-zA-Z_]{6,12}$
- 手机号正则参考:
^1\d{10}$
......
增加HTTP安全头字段:X-Content-Type-Options
和X-XSS-Protection
-
增加
X-Content-Type-Options:nosniff
的HTTP安全头字段,可避免部分版本IE浏览器无视Content-Type
设置执行XSS Payload。 -
增加
X-XSS-Protection:1;mode=block
的HTTP安全头字段可提示浏览器发现XSS Payload时不要渲染文档
使用内置转义函数
当使用PHP、java、Nodejs的后端服务器接收到内容需要输出到HTML标签内时,都可以使用相对应的转义函数转义,将会对< > ' " &
等字符转义。
DOM型XSS防范
查看代码是否有document.write、eval、window之类能造成危害的地方,利用第三放的过滤方法filter.js进行过滤。
var url = getParam('url')
url = ValidURL(url) //调用filter.js的函数进行过滤
location.href = url