前言
上一篇文章中介绍了XSS,这篇文章介绍CSRF
CSRF
CSRF是什么
Cross-site request forgery, 跨站请求伪造。是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起跨站请求。
我自己模拟了一个例子,
- 首先登录www.abc.com
在这个页面内,我会自动向服务器发起一个get请求,服务器响应这个请求的同时会向浏览器发送cookie
- 在同一浏览器下登录www.haha.com
在这个页面内,做了三个事情
- 利用img标签自动向localhost:8899发送了一个get请求
- 利用js自动向localhost:8899发送了一个post请求
- 通过引诱用户点击a标签,向localhost:8899发送了一个post请求
通过截图可以看到,三个请求都带了www.abc.com下的cookie发送给了localhost:8899服务器,并且localhost:8899服务器也正常响应了。
流程图如下:
防止CSRF
黑客发起CSRF攻击的条件
- 目标网站一定要有CSRF漏洞,黑客破解了服务器的接口
- 用户登录过目标网站,并且浏览器保存目标网站的登录状态
- 用户在同一浏览器下以某种方式打开了黑客的网站或者是攻击的链接
CSRF攻击与XSS攻击不同,CSRF攻击不会往页面内注入恶意脚本,因此黑客是无法通过CSRF攻击来获取用户页面数据的,所以主要由服务器来做预防。
主要有以下几种方式:
- 充分利用好cookie的SameSite属性
SameSite选项通常由Strict、Lax和None三个值
- Strict最为严格,如果cookie设置了Strict,那么浏览器会完全禁止第三方Cookie。
- Lax相对宽松一点,在跨站点的情况下,从第三方站点的链接打开和从第三方站点提交Get的表单都会携带cookie.但是如果在第三方站点中使用Post方法或者通过img、iframe等标签加载的URL,都不会携带Cookie。
- None, 任何情况下都会发送Cookie。
但是现在大部分的网站静态资源都放在单独的域名下,所以通过设置Cookie的SameSite为Strict、Lax是不能正常运行的,所以这个方法只适用静态资源跟服务器接口在同一个站点下的网站。
我测试了一下,如下图:
localhost:8899向www.abc.com写入SameSite值为Lax的cookie,写不进去
locahost:8899向该站点下的localhost:8899/get写入SameSite值为Lax的cookie,成功写入。
- 验证请求的来源站点
在服务器端验证请求的来源站点。因为CSRF攻击大多数都是来自第三方站点。
通过http请求头中的Referer和Origin属性
- referer属性
记录了该http请求的来源地址,但有些场景不适合将来源URL暴露给服务器,所以可以设置不用上传,并且referer属性是可以修改的,所以在服务器端校验referer属性并没有那么可靠
- origin属性
通过XMLHttpRequest、Fetch发起的跨站请求或者Post方法发送请求时,都会带上origin,所以服务器可以优先判断Origin属性,再根据实际情况判断是否使用referer判断。
- CSRF Token
除了上面两个方法之外,还可以使用CSRF Token来验证。
- 在浏览器向服务器发起请求时,服务器生成一个CSRF Token(字符串)发送给浏览器,然后将该字符串放入页面中
- 浏览器请求时(如表单提交)需要带上这个CSRF Token。服务器收到请求后,验证CSRF是否合法,如果不合法拒绝即可。
代码
代码很简单
- www.abc.com:3000
<div>
<h6>CSRF测试</h2>
<a href="http://www.haha.com:9999">点我点我</a>
<script>
fetch('http://localhost:8899', {
method: 'get',
mode: 'cors',
credentials: 'include' //很重要,允许跨域访问传输cookie
}).then((res) => {
console.log(res)
})
</script>
</div>
- 服务器 localhost:8899
const express = require('express')
const app = express()
const port = 8899
//allow custom header and CORS
app.all('*',function (req, res, next) {
// res.header('Access-Control-Allow-Origin', '*');
// res.header('Access-Control-Allow-Origin', req.hostname);
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
res.header('Access-Control-Allow-Credentials', true); // 很重要,允许跨域访问传输cookie
if (req.method == 'OPTIONS') {
res.send(200); /让options请求快速返回/
}
else {
next();
}
});
app.get('/', (req, res) => {
// res.cookie('name', 'hey', { domain: req.hostname, path: '/'});
res.cookie('name', 'hey', { domain: req.hostname, path: '/', sameSite: 'None'});
// res.cookie('name', 'hey', { secure: true });
res.send('Hello World!a')
})
app.get('/get/test', (req, res) => {
res.cookie('username', 'hahah', { domain: req.hostname, path: '/', sameSite: 'Lax'});
// res.cookie('name', 'hey', { secure: true });
res.send('Hello World!a')
})
app.get('/get', (req, res) => {
res.set('Content-Type', 'text/html')
console.log(req.headers, 'header')
const html = `
<div>
<div>服务器同站点下的页面</div>
<script>
function fetchUrl(url, method='get') {
fetch(url, {
method,
mode: 'cors',
credentials: 'include' //很重要,允许跨域访问传输cookie
}).then((res) => {
console.log(res)
})
}
fetchUrl('http://localhost:8899/get/test')
</script>
</div>
`
res.send(html)
})
app.post('/post', (req, res) => {
console.log(req.headers, 'header')
// res.cookie('name', 'hey', { secure: true });
res.send('I am post')
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
- 服务器 www.haha.com:9999
const express = require('express')
const app = express()
const port = 9999
//allow custom header and CORS
app.all('*',function (req, res, next) {
// res.header('Access-Control-Allow-Origin', '*');
// res.header('Access-Control-Allow-Origin', req.hostname);
console.log(req.headers.origin)
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
res.header('Access-Control-Allow-Credentials', true); // 很重要,允许跨域访问传输cookie
if (req.method == 'OPTIONS') {
res.send(200); /让options请求快速返回/
}
else {
next();
}
});
app.get('/', (req, res) => {
res.set('Content-Type', 'text/html')
console.log(req.headers, 'header')
const html = `
<div>
<div>nihao</div>
<img src="http://localhost:8899" />
<a href="javascript: fetchUrl('http://localhost:8899/post', 'post');">你好,交个朋友吧</a>
<script>
function fetchUrl(url, method='get') {
fetch(url, {
method,
mode: 'cors',
credentials: 'include' //很重要,允许跨域访问传输cookie
}).then((res) => {
console.log(res)
})
}
fetchUrl('http://localhost:8899/post', 'post')
</script>
</div>
`
res.send(html)
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
最后
之前虽然知道CSRF的原理,但是没有实际模拟过,实际模拟之后,感觉,原来真的那么简单。也算是一个小小收获
欢迎跟我一起挖坑、填坑。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。