什么是XSS?
众所周知XSS是Cross-Site Scripting(跨站脚本攻击)的简称,但是英文的缩写明明是CSS为什么叫XSS呢?———历史遗留问题,因为CSS层叠样式表(Cascading Style Sheets)已经被大家所熟知,怕有所混淆,X的发音可能是与Cross发音有那么一丢丢相似,所以改名为XSS啦
言归正传,XSS是一种代码注入攻击,通常指的是利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,恶意代码未经过滤,与网站正常的代码混在一起,浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
XSS是最普遍的Web应用安全漏洞。由于直接在用户的终端执行,恶意代码能够直接获取用户的信息如 Cookie、SessionID 等,进而危害数据安全或者利用这些信息冒充用户向网站发起攻击者定义的请求。
XSS分类
根据攻击来源,XXS攻击分为存储型、反射型 和 DOM 型
三种。
存储型
存储型 XSS 的攻击步骤 :
- 攻击者将恶意代码提交到目标网站的
数据库
中。 - 用户打开目标网站时,网站
服务端
将恶意代码从数据库取出
,拼接在 HTML中返回给浏览器。 - 用户浏览器接收到响应后解析执行,混在其中的恶意代码里被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
常见的场景是攻击者在社区或论坛上写下一篇包含恶意 JavaScript 代码的文章或评论,文章或评论发表后,所有访问该文章或评论的用户,都会在他们的浏览器中执行这段恶意的 JavaScript 代码。这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论等。
例:当用户点击提交按钮将输入信息提交到服务端时,服务端通过 userInput
变量保存了输入内容。
html代码
<h2>测试存储型XSS攻击</h2>
<input type="text" id="input">
<button id="btn">提交</button>
<script type="text/javascript">
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:8008/submit', {
method: 'POST',
body: val
});
}, false);
</script>
Node.js代码
const http = require('http');
let userInput = '';
function handleReequest(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 === '/submit') {
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(8008, '127.0.0.1');
server.on('request', handleReequest);
如下图所示:
当用户通过 http://localhost:8008/${id}
访问时,服务端会返回与 id 对应的内容。如果用户输入了恶意脚本内容,则其他用户访问该内容时,恶意脚本就会在浏览器端执行:
反射型
反射型 XSS 的攻击步骤:
- 攻击者构
造出特殊的URL
,其中包含恶意代码。 - 用户打开带有恶意代码的 URL 时,网站
服务端
将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。 - 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
例:一个恶意链接的地址指向了 http://127.0.0.1:8008//?test=1&request=2。
如下图所示:
然后,我再启一个简单的 Node 服务处理恶意链接的请求,代码如下:
//创建一个nodejs服务器
let http =require('http');
let app = http.createServer((req,res)=>{
// req:请求对象,包含一些查询参数、请求体,请求路径,cookie 请求域..
// res :响应对象
res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
res.write('<script>alert("反射型 XSS 攻击")</script>');
})
// 监听端口
app.listen(8008,()=>{
console.log('the server running at http://127.0.0.1:8008');
})
当用户点击恶意链接时,页面跳转到攻击者预先准备的页面,会发现在攻击者的页面执行了 js 脚本,这样就产生了反射型 XSS 攻击。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。
DOM型
DOM 型 XSS 的攻击步骤:
- 攻击者构造出
特殊的 URL
,其中包含恶意代码。 - 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,
前端 JavaScript 取出 URL中的恶意代码并执行
。 - 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
例:
<h2>测试DOM型XSS攻击</h2>
<input type="text" id="input">
<button id="btn">提交</button>
<div id="div"></div>
<script type="text/javascript">
const input = document.getElementById('input');
const btn = document.getElementById('btn');
const div = document.getElementById('div');
let val;
input.addEventListener('change', (e) => {
val = e.target.value;
}, false);
btn.addEventListener('click', () => {
div.innerHTML = `<a href=${val}>恶意链接</a>`
}, false);
</script>
点击提交按钮后,会在当前页面插入一个链接,其地址为用户的输入内容。如果用户在输入时构造了: <a href onlick=“alert(/基于DOM的XSS攻击/)”>恶意链接</a>
,此时,用户点击生成的链接,就会执行对应的脚本。效果图如下所示:
DOM 型 XSS 攻击中,构造的URL参数不用发送到服务器端,可以达到绕过WAF、躲避服务端的检测效果,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而存储型 XSS和反射型 XSS 都属于服务端的安全漏洞。
XSS攻击的危害:
- 盗用 cookie 实现无密码登录,盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
- 配合 csrf 攻击完成恶意请求,控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
- 非法转账,强制发送电子邮件
- 控制受害者机器向其它网站发起攻击
- 使用 js 或 css 破坏页面正常的结构与样式等;
XSS攻击的预防
XSS 攻击有两大要素:
- 攻击者
提交恶意代码
。 浏览器执行
恶意代码。
输入过滤
- 由前端过滤输入,然后提交到后端,这样并不能预防,一旦攻击者绕过前端过滤,直接构造请求,就可以提交恶意代码了。
- 后端在写入数据库前,对输入进行过滤,这会产生一个问题,用户输入的内容,如果被转义,前端展示出来的内容,可能是HTML直接展示转义后的内容,可能是由后盾返回,给JS变量的值,前端得到的字符串就是转义后的字符串
所以,输入过滤能够在某些情况下解决特定的 XSS 问题,但会引入很大的不确定性和乱码问题,所以我们要通过防止浏览器执行恶意代码来防范XSS
。
对所有用户提交内容进行可靠的输入验证
。这些提交内容包括URL、查询关键字、http头、post数据等。只接受在你所规定长度范围内、采用适当格式、你所希望的字符。阻塞、过滤或者忽略其它的任何东西。
预防存储型和反射型XSS攻击
存储型和反射型XSS攻击都是在服务端取出恶意代码后,插入到响应 HTML 里的,攻击者刻意编写的“数据”被内嵌到“代码”中,被浏览器所执行。
两种常见的做法:
- 改成纯前端渲染,把代码和数据分隔开。
- 对 HTML 做充分转义。
纯前端渲染的过程:
- 浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据。
- 然后浏览器执行 HTML 中的 JavaScript。
- JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。
我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText),还是属性(.setAttribute),还是样式(.style)等等。这样浏览器可以区分即将执行的代码都是什么类型的代码,不会轻易被欺骗。但对于性能要求高,或有SEO需求的页面,我们仍然要面对拼接HTML的问题
如果拼接HTML是必要的,就需要采用合适的转义库,对HTML模板各处插入点进行充分的转义。
确认你接收的HTML内容被妥善地格式化,仅包含最小化的、安全的tag(绝对没有JavaScript),去掉任何对远程内容的引用(尤其是样式表和JavaScript)。为了更多的安全,请使用httpOnly的cookie。尽量避免拼接HTML
预防 DOM 型 XSS 攻击
避免内联事件,如果用 Vue/React 技术栈,并且不使用 v-html/dangerouslySetInnerHTML 功能,就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患。
DOM 中的内联事件监听器,如 location、onclick、onerror、onload、onmouseover
等,<a> 标签的 href 属性
,JavaScript 的 eval()、setTimeout()、setInterval()
等,都能把字符串作为代码运行。一定要避免在字符串中拼接不可信数据。
此外httpOnly、CSP、X-XSS-Protection、Secure Cookie 等也可以起到有效的防护。
参考文献: