今天,我们来谈谈 XSS
攻击。
XSS 是什么
XSS
攻击指的是攻击者通过在受信任的网站上注入恶意的脚本,使得用户的浏览器在访问该网站时执行这些恶意脚本,从而导致信息泄露等安全问题。
XSS
英文名Cross-Site-Scripting
,即跨站脚本。为什么不叫CSS
呢?因为CSS
的缩写已经被Cascading Style Sheets
,即层叠样式表占用。
XSS 分类和演示
XSS
攻击主要分成三类:DOM
型 XSS
攻击、反射型 XSS
攻击和存储型 XSS
攻击。
我们接下来需要演示下 XSS
攻击,我们做点前期准备。
案例的演示环境:
macOS Monterey
- Apple M1node version
- v14.18.1Visual Studio Code
及其Live Server
插件
首先,我们添加个 hostname
, 方便测试,当然你可以直接使用 ip
地址测试。
通过 sudo vim /etc/hosts
添加 127.0.0.1 a.example.com
的映射:
本文所有的案例通过 SSR
应用进行演示。
DOM
型 XSS
攻击
DOM
型 XSS
攻击利用了前端 Javascript
在浏览器中动态操作 DOM
的特性。DOM
型 XSS
攻击的原理是攻击者通过注入恶意代码或者脚本到网页中的 DOM
元素中,然后通过浏览器执行这些恶意的代码。
案例,如下:
const Koa = require('koa'); const Router = require('koa-router'); const views = require('koa-views'); const path = require('path');
const app = new Koa();
const router = new Router();app.use(
views(path.join(__dirname, 'views'), {
extension: 'ejs'
})
);router.get('/', async (ctx) => {
await ctx.render('index', {
xss: '<script>alert("XSS")</script>',
content: 'DOM - XSS Attack'
})
});
app.use(router.routes());
app.listen(3000, () => {
console.log("listening on http://localhost:3000");
})
上面👆,我们渲染的模版(如下)index
,并将数据 xss
和 content
传递给模版。
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DOM - XSS - Jimmy</title>
</head>
<body>
<h3 style="text-align: center;"><%= content %></h3>
<%- xss %>
</body>
</html>
在模版中,我们读取了字符串 content
,和 html
数据 xss
。运行之后,会弹出攻击成功的提示:
反射型 XSS
攻击
反射型 XSS
攻击,指攻击者通过构造恶意的 URL
,利用用户的输入参数将恶意的代码注入到目标站点的响应内容中,然后将注入的恶意代码发送给浏览器执行,从而实现攻击。简而言之:就是把用户输入的数据从服务端反射给用户浏览器。
下面是一个小案例:
const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');const app = new Koa();
const router = new Router();app.use(
views(path.join(__dirname, 'views'), {
extension: 'ejs'
})
);router.get('/', async (ctx) => {
await ctx.render('index')
});router.get('/api/username', async (ctx) => {
let url = ctx.request.url;
let _username = '';
if(url.split('username=')[1]) {
_username = url.split('username=')[1];
}
ctx.body = {
username: _username // <img src="XX" onerror='alert("XSS")' style="display: inline-block; width: 0; height: 0;"/> Jimmy
}
})
app.use(router.routes());
app.listen(3000, () => {
console.log("listening on http://localhost:3000");
})
上面我们渲染了首页 index
模版,然后提供了一个 /api/username
的接口,返回 username
数据。
首页模版如下:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>反射型 - XSS - Jimmy</title>
</head>
<body>
<h3 style="text-align: center;">反射型 - XSS</h3>
<textarea type="text" id="input" placeholder="please enter username" rows="8"></textarea>
<button id="trigger">Say Hi</button>
<p style="color: #f00;" id="hint"></p>
<script>
let inputDom = document.getElementById("input");
let triggerDom = document.getElementById("trigger");
let hintDom = document.getElementById("hint");
triggerDom.addEventListener("click", () => {
const params = {
username: inputDom.value
}
const queryString = Object.keys(params)
.map(key => encodeURIComponent(key) + "=" + encodeURIComponent(params[key]))
.join("&");fetch(`/api/username?${queryString}`, { method: "GET" }) .then(response => response.json()) .then(data => { let _username = data.username; hintDom.innerHTML = `Hello, ${ decodeURIComponent(_username) }.`; }) })
</script>
</body>
</html>
我们提供了一个输入框 textarea
,然后触发按钮,调用接口获取返回的数据,然后在页面中展示 username
。比如,Jimmy
写入输入框,会在页面展示 Hello, Jimmy.
的效果。但是,对于 Hacker
来说,我可以输入:
<img src="XX" onerror='alert("XSS")' style="display: inline-block; width: 0; height: 0;"/> Jimmy
页面的展示内容还是 Hello, Jimmy.
,却多做了弹窗的动作。
注意⚠️ 现代浏览器通常会自动阻止通过
innerHTML
插入的包含脚本的内容
储存型 XSS
攻击
存储型攻击,指攻击者利用它在目标站点上储存的恶意脚本,当用户访问该页面时,恶意脚本被执行。该类攻击在评论区
常见。
一般的攻击步骤:
hacker
在评论区输入了攻击的脚本- 服务度端存储了该脚本
- 用户浏览网页,拉取了该脚本
- 脚本执行,攻击生效
const Koa = require('koa'); const Router = require('koa-router'); const views = require('koa-views'); const path = require('path');
const app = new Koa();
const router = new Router();const dataList = [{
name: 'Ivy',
comment: 'Nice Day!'
}]app.use(
views(path.join(__dirname, 'views'), {
extension: 'ejs'
})
);router.get('/', async (ctx) => {
await ctx.render('index')
});router.get('/api/comments', async (ctx) => {
ctx.body = {
list: dataList
}
})router.get('/api/comment/add', async (ctx) => {
let query = ctx.request.url.split('?')[1];
let usernamePart = query.split('&')[0];
let commentPart = query.split('&')[1];
dataList.push({
name: usernamePart.split('=')[1],
comment: commentPart.split('=')[1]
})
ctx.body = {
message: 'ok'
}
})
app.use(router.routes());
app.listen(3000, () => {
console.log("listening on http://localhost:3000");
})
我们通过变量 dataList
来模拟从数据库数据。接口 /api/comment/add
是添加评论,接口 /api/comments
是拉取评论,读取 dataList
变量值。
模版的设置如下:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>存储型 - XSS - Jimmy</title>
</head>
<body>
<h3 style="text-align: center;">存储型 - XSS</h3>
<textarea type="text" id="input" placeholder="comment here..." rows="8"></textarea>
<button id="trigger">Comment</button>
<ul id="list"></ul>
<script>
let inputDom = document.getElementById("input");
let triggerDom = document.getElementById("trigger");
let listDom = document.getElementById("list");initList(); triggerDom.addEventListener("click", () => { const params = { username: 'Jimmy', comment: inputDom.value // <img src="XX" onerror='alert("XSS")' style="display: inline-block; width: 0; height: 0;"/> XSS happen. } const queryString = Object.keys(params) .map(key => encodeURIComponent(key) + "=" + encodeURIComponent(params[key])) .join("&"); fetch(`/api/comment/add?${queryString}`, { method: "GET" }) .then(response => response.json()) .then(data => { if(data.message === 'ok') { initList(); } }) }) function initList() { fetch('/api/comments') .then(response => response.json()) .then(data => { let list = data.list; let domLis = ``; for(let i = 0; i < list.length; i += 1) { let person = list[i]; domLis += `<li><b>${ person.name }</b> said: ${ decodeURIComponent(person.comment) }</li>` } listDom.innerHTML = domLis; }) }
</script>
</body>
</html>
我们一进来页面,默认拉取了评论列表。触发按钮,添加评论,当评论添加成功后,重新拉取评论列表数据。
XSS 避免
那么,我们应该如何避免 XSS
攻击呢?
- 输入验证和过滤:用户输入的内容不能相信,要对用户输入的数据进行验证,只接受可信任的数据。比如对脚本标签
script
处理,剔除该标签的潜在危险 - 使用安全的框架或者库:比如选择前端开发框架
Angular
,其内置了安全机制,默认XSS
防护;又比如你可以使用库 xss 来避免此类攻击 - 设置 HTTP 头部:我们可以设置适当的
HTTP
头部,比如Content-Security-Policy (CSP) 内容安全策略
,X-XSS-Protection
等。增强应用程序的安全性。
CSP
比如:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src data: https://images.example.com; font-src 'self' https://fonts.gstatic.com;
default-src
指定了默认的加载源限制为智能从同源地址加载。script-src
指定了可以从同源地址和 https://cdn.example.com
地址加载脚本。后面的 img
和 font
类推。
当然,定期更新和修补漏洞也是不可少的。减少给 Hacker
攻击的机会。
参考
- Cross-site scripting(跨站脚本攻击)
- Figma
- xss
- ejs