CSRF(Cross-site request forgery)跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常也被称为 CSRF 或者 XSRF。
CSRF 攻击
攻击原理
攻击者诱导被害者进入第三方网站,在第三方网站中利用被害者已经获得了登录凭证或授权信息,冒充被害者向目标网站发送请求,从而达到攻击。此过程不需要获取用户得登录凭证或者是授权信息,而是通过浏览器的特性。
攻击步骤
- 用户 A 访问目标网站 B,登录成功
- 在登录凭证未失效的情况下访问具有攻击性的网站 C
- C 网站向目标网站 B 发送请求,浏览器会默认带上 B 网站的 Cookie
- 后端接收到请求,误以为是用户 A 在 B 网站发送的请求,正常返回
- 攻击完成
场景
目标网站 B
地址 http://localhost:3000
目标网站 B 的首页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>目标网站</title>
</head>
<body>
<h3>首页</h3>
{{if user}}
<p>你好,{{ user.username }}</p>
{{/if}}
{{if !user}}
<div>
<a href="/login">登录</a>
</div>
{{/if}}
</body>
</html>
目标网站 B 的登录页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录页面</title>
</head>
<body>
<form action="/login" method="POST">
<div>
<label for="">用户名:</label>
<input type="text" name="username">
</div>
<div>
<label for="">密码:</label>
<input type="text" name="password">
</div>
<div>
<button>登录</button>
</div>
</form>
</body>
</html>
目标网站 B 的后端服务器
const express = require('express')
const session = require('express-session')
const app = express()
app.engine('html', require('express-art-template'))
// 解析表达请求体数据
app.use(express.urlencoded({
extended: true
}))
// 设置 Session
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}))
// CSRF
// 首页
app.get('/', (req, res) => {
res.render('index.html', {
user: req.session.user
})
})
// 登录页
app.get('/login', (req, res) => {
res.render('login.html')
})
// 登录
app.post('/login', (req, res) => {
const user = req.body
console.log(user)
if (user && user.username === 'yzh' && user.password === '123456') {
// 登录成功
// 1.记录登录状态
// 2.跳转到首页
req.session.user = user
return res.redirect('/')
}
res.status(401).send('失败了')
})
// 评论
app.post('/comment', (req, res) => {
if (!req.session.user) {
return res.status(401).send('未授权')
}
res.send('评论成功')
})
app.listen(3000, () => {
console.log('服务器启动成功:http://localhost:3000')
})
第三方网站 C
地址 http://localhost:5000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="http://localhost:3000/comment" method="post">
<input type="text" name="username" hidden>
<input type="text" name="password" hidden>
<input type="text" name="account" hidden>
</form>
<script>
document.forms[0].submit()
</script>
</body>
</html>
攻击步骤
- 用户 A 登录 目标网站 B
- 登录凭证未过期
- 访问第三方网站 C
- 第三方网站 C 向目标网站 B 发送请求(默认携带 B 网站的 Cookie)
- 攻击完成
CSRF 攻击分类
GET 请求
<img src="http://example/comment?id=998&content=xxx" />
被害者访问的含有上述 img 的网站,网站会自动加载该图片,然后发送一个 http://example/comment?id=998&content=xxx 链接的 GET 请求,example 服务器就会接收到一个跨域的 GET 请求。
POST 请求
<form action="http://localhost:3000/comment" method="post">
<input type="text" name="username" hidden>
<input type="text" name="password" hidden>
<input type="text" name="account" hidden>
</form>
<script>
document.forms[0].submit()
</script>
被害者访问了含上述表单的网站,网站会自动发送一个 POST 请求。
链接类型
<a href="http://example/comment?id=998&content=xxx">送500元现金了!!!先到先得</a>
此类型和上面的 GET 请求原理一样,只是一个是用户主动触发,一个是被动触发。当被害者点击该链接就会发送一个跨域请求。
攻击特点:
1、一般发生在第三方
2、攻击者无法获取 Cookie,只能使用
CSRF 防御
阻止不明域名的请求
- 同源检测
- SameSite
提交时要求附加本域才能获取的信息
- CSRF Token
- 双重 Cookie 验证
同源检测
主要是通过检测,过滤第三方域名的请求。
那我们如何来检测过滤第三方的请求呢?
在 HTTP 请求时,请求头都会携带 Origin 和 Referer 标识来识别源地址。
后端服务器可以通过检测这两个标识进行过滤。
SameSite
SameSite 是 Set-Cookie 的属性,允许你声明 Cookie 是否应限于第一方或同一站点上下文。说白就是第三方发起请求时是否能访问到你的 Cookie。
分别有三个属性值:
- Strict
- Lax
- None
Strict
最为严格,请求时完全禁止第三方发送 Cookie,只有当前请求 URL 和目标网站一致才会发送 Cookie。
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
Lax
Chrome 浏览器默认,相较于 Strict 限制有所放松,只有在导航到目标地址时发送的 GET 请求才会带上 Cookie,其他情况下基本不会发送。
Set-Cookie: CookieName=CookieValue; SameSite=Lax;
| 请求类型 | 实例 | 正常情况 | Lax |
|---|---|---|---|
| 链接 | <a href="xxx"></a> | 发送 Cookie | 发送 Cookie |
| 预加载 | <link rel="prerender" href="xxx" /> | 发送 Cookie | 发送 Cookie |
| GET 表单 | <form method="GET" action="xxx" | 发送 Cookie | 发送 Cookie |
| POST 表单 | <form method="POST" action="xxx"> | 发送 Cookie | 不发送 |
| iframe | <iframe src="xxx"></iframe> | 发送 Cookie | 不发送 |
| ajax | $.get("xxx") | 发送 Cookie | 不发送 |
| Image | <img src="xxx" /> | 发送 Cookie | 不发送 |
在用户浏览器支持 SameSite 时,设置 Strict 和 Lax 后,基本能防御 CSRF 攻击
None
不设置 SameSite 属性,不过必须得和 Secure 一起设置才能生效。
Set-Cookie: widget_session=abc123; SameSite=None; Secure
CSRF Token
前面讲到CSRF的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。
而CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。
双重 Cookie 验证
利用 CSRF 攻击不能获取到用户Cookie的特点,我们可以要求 Ajax 和表单请求携带一个 Cookie 中的值。
例如:发送一个 GET 请求,我们可以在请求地址的后面加上 Cookie 的参数,csrfCookie 的值等于 cookie 的值。
$.get("http://example.com/api/test?csrfCookie=xxxx")
后端服务器接收到请求后,判断 csrfCookie 和 Cookie 的值是否一致,如果不一致则直接拒绝。
由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间)
所以会存在用户访问的网站为 www.a.com ,而后端的 api 域名为 api.a.com。那么在 www.a.com 下,前端拿不到 api.a.com 的Cookie,也就无法完成双重Cookie认证。