CSRF
一、什么是CSRF
- CSRF(Cross-site request forgery)跨站请求伪造
- 攻击者诱导受害者进入第三方网站 ,在第三方网站中,向被攻击网站 发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的
二、CSRF的攻击类型
GET类型的CSRF
- 用户信任
http://bank.com
网站,并且保存登录状态 - 诱导用户点击 图片
<img src="http://bank.com/transfer?to=attacker&amount=1000">
http://bank.com
网站误以为是用户自己发送的请求,并以用户的名义执行了攻击者自定义的操作,触发转账
POST类型的CSRF
-
构造隐藏表单 ,利用JavaScript自动提交表单到目标网站
html<form action="http://bank.example/withdraw" method=POST> <input type="hidden" name="account" value="xiaoming" /> <input type="hidden" name="amount" value="10000" /> <input type="hidden" name="for" value="hacker" /> </form> <script> document.forms[0].submit(); </script>
链接类型的CSRF
-
在论坛中发布的图片中嵌入**恶意链接,**或者以广告的形式诱导用户中招
html<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank"> 重磅消息!! <a/>
三、防护策略
CSRF的特点与策略
##### 特点
* CSRF(通常)发生在第三方域名。
* CSRF攻击者不能获取到Cookie等信息,只是使用。
##### 对应策略
* **阻止不明外域的访问**
* 同源检测
* Samesite Cookie
* **提交时要求附加本域才能获取的信息**
* CSRF Token
* 双重Cookie验证
同源检测
##### 原理
* 检查请求的 `Referer` 或 `Origin` 头是否来自**合法域名**。
* **注意** :部分浏览器可能不发送 `Referer`(如隐私模式),需结合其他防御手段。
```js
// 后端
app.post('/transfer', (req, res) => {
const referer = req.get('Referer');
const origin = req.get('Origin');
const allowedDomains = ['https://your-legitimate-site.com'];
if (!allowedDomains.includes(referer) && !allowedDomains.includes(origin)) {
return res.status(403).send('Invalid request source');
}
// 处理合法请求
});
```
Samesite Cookie
-
Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个"同站 Cookie",同站Cookie只能作为第一方Cookie,不能作为第三方Cookie。
-
假如淘宝网站用来识别用户登录与否的 Cookie 被设置成了 Samesite=Strict,那么用户从百度搜索页面甚至天猫页面的链接点击进入淘宝后,淘宝都不会是登录状态,因为淘宝的服务器不会接受到那个Cookie,其它网站发起的对淘宝的任意请求都不会带上那个 Cookie。
##### Samesite 有以下属性值
| 模式 | 行为 | 适用场景 |
|:-----------|:---------------------------------------------------|:---------------------|
| **Strict** | **完全禁止**跨站请求携带 Cookie(包括用户点击其他站点的链接跳转) | 极高敏感操作(如支付、修改密码) |
| **Lax** | 允许**安全的跨站** GET 请求(如导航到目标站的链接)携带 Cookie | 大多数场景(浏览器默认行为) |
| **None** | 允许**所有跨站请求** 携带 Cookie,但必须同时设置 `Secure` 属性(仅HTTPS) | 需要跨站 Cookie 的第三方集成场景 |
```js
// 后端设置Cookie
app.post('/api/login', (req, res) => {
res.cookie('CSRF-TOKEN', crypto.randomBytes(32).toString('hex'), {
secure: true,
sameSite: 'Lax',
httpOnly: false // 允许前端读取
});
res.send('OK');
});
// 验证
app.post('/api/transfer', (req, res) => {
if (req.cookies['CSRF-TOKEN'] !== req.headers['x-csrf-token']) {
return res.status(403).send('CSRF验证失败');
}
// 处理业务逻辑
});
```
```js
// 前端Axios全局配置自动携带Token
axios.interceptors.request.use(config => {
const token = document.cookie.match(/CSRF-TOKEN=([^;]+)/)?.[1];
config.headers['X-CSRF-TOKEN'] = token;
return config;
});
```
CSRF Token
##### 原理
* 服务器生成一个**随机且唯一** 的 Token,嵌入**表单或 HTTP 请求头**中,提交时验证 Token 是否合法。
* Token 需满足:每次会话或请求唯一、加密随机、不可预测。
```js
// 后端
HttpServletRequest req = (HttpServletRequest)request;
HttpSession s = req.getSession();
// 从 session 中得到 csrftoken 属性
String sToken = (String)s.getAttribute("csrftoken");
if(sToken == null){
// 产生新的 token 放入 session 中
sToken = generateToken();
s.setAttribute("csrftoken",sToken);
chain.doFilter(request, response);
} else{
// 从 HTTP 头中取得 csrftoken
String xhrToken = req.getHeader("csrftoken");
// 从请求参数中取得 csrftoken
String pToken = req.getParameter("csrftoken");
if(sToken != null && xhrToken != null && sToken.equals(xhrToken)){
chain.doFilter(request, response);
}else if(sToken != null && pToken != null && sToken.equals(pToken)){
chain.doFilter(request, response);
}else{
request.getRequestDispatcher("error.jsp").forward(request,response);
}
}
```
```html
<!-- 前端表单嵌入 --!>
<!-- 后端渲染页面时注入 Token -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{ csrfToken }}"> <!-- 例如通过模板引擎注入 -->
<input type="number" name="amount">
<button>Submit</button>
</form>
```
```js
// 前端AJAX请求
// 从 Meta 标签或 Cookie 获取 Token(需后端配合)
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
// 发送请求时携带 Token
axios('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken // 通过 HTTP 头传递
},
body: JSON.stringify({ amount: 100 })
});
```
双重Cookie验证
##### 原理
* **无状态验证**:无需服务器存储Token,通过客户端自行携带两份Token副本。
* **跨域限制**:攻击者无法通过跨域请求同时篡改请求参数和Cookie。
* **动态绑定**:Token与用户会话或请求上下文动态绑定,增强安全性。
##### 流程
* **生成Token** :
* 用户登录时,服务器生成随机Token,**写入Cookie** (如`CSRF-Token=abc123`)。
* **前端传递Token** :
* 前端通过JavaScript读取**Cookie中的Token**,将其添加到请求参数或HTTP头中。
* **服务器验证** :
* 服务器比对请求**参数/头中的Token** 与**Cookie中的Token**是否一致。
##### 与CSRF的区别
| **特性** | **双重Cookie验证** | **传统CSRF Token** |
|:--------|:-------------------|:---------------------------|
| 服务端状态管理 | 无状态 | 需存储Token(Session/DB/Redis) |
| 实现复杂度 | 简单,无需存储逻辑 | 需处理Token生成、存储、验证 |
| 抗XSS能力 | 弱(依赖前端读取Cookie) | 强(Token可设置为HttpOnly) |
| 跨域支持 | 需处理CORS和Cookie跨域策略 | 需处理Token传递方式 |
```js
// 后端设置Cookie并验证
// server.js
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.use(cookieParser());
// 配置CORS(根据实际部署调整)
app.use(cors({
origin: 'http://localhost:8080', // Vue前端地址
credentials: true // 允许携带Cookie
}));
// 登录接口:生成Token并设置Cookie
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
// 模拟用户验证
if (username === 'admin' && password === '123456') {
// 生成随机Token
const csrfToken = crypto.randomBytes(32).toString('hex');
// 设置Token到Cookie(允许前端读取)
res.cookie('CSRF-TOKEN', csrfToken, {
httpOnly: false, // 必须允许前端JS读取
secure: process.env.NODE_ENV === 'production',
sameSite: 'Lax', // 允许安全跨站请求
maxAge: 86400000 // 24小时有效期
});
return res.json({ success: true });
}
res.status(401).json({ error: '认证失败' });
});
// 敏感操作接口:验证双重Cookie
app.post('/api/transfer', (req, res) => {
const cookieToken = req.cookies['CSRF-TOKEN'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF验证失败' });
}
// 处理业务逻辑
res.json({ success: true, amount: req.body.amount });
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
```
```js
// 前端配置Axios全局拦截器
import axios from 'axios';
const instance = axios.create({
baseURL: 'http://localhost:3000/api',
withCredentials: true // 允许跨域携带Cookie
});
// 请求拦截器:自动添加Token
instance.interceptors.request.use(config => {
// 从Cookie中获取Token
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('CSRF-TOKEN='))
?.split('=')[1];
if (csrfToken) {
config.headers['X-CSRF-TOKEN'] = csrfToken;
}
return config;
});
export default instance;
```
分布式校验
##### 原理
* **分布式Token管理**:统一生成、存储和验证Token,确保跨服务请求的合法性。
* **动态同步机制**:解决多节点间的Token状态同步问题。
* **去中心化验证**:减少对单点服务的依赖,提升系统可用性。
##### 中央认证服务(推荐)
* **原理**:由中心化服务统一生成Token,各服务节点通过API验证Token。
- 使用Session存储CSRF Token会带来很大的压力,使用计算出来的Token,而非随机生成的字符串Token,这样在校验时无需再去读取存储的Token,只用再次计算一次即可(Encrypted Token Pattern方式)
总结
##### **基础防御组合:CSRF Token + SameSite Cookie**
*
##### **适用场景**:
* **传统多页面Web应用**(如Django、Rails应用)
* **混合型架构**(部分服务端渲染 + 部分AJAX请求)
*
##### **实现方式**:
* **CSRF Token**:
* 服务端生成随机Token,嵌入表单或HTTP头(如`X-CSRF-TOKEN`)。
* 提交请求时验证Token合法性。
* **SameSite Cookie**:
* 设置会话Cookie的`SameSite=Lax`,限制跨站请求自动携带Cookie。
* 敏感操作Cookie设为`SameSite=Strict`(如支付确认)。
##### **增强防御组合:加密Token + 双重Cookie验证**
*
##### **适用场景**:
* **单页应用(SPA)**(如Vue、React应用)
* **无状态API服务**(如RESTful接口)
* **实现方式**:
* **加密Token**:
* 使用AES或HMAC加密Token,嵌入用户ID、时间戳等元数据。
* 防止Token被窃取后直接重放。
* **双重Cookie验证**:
* 服务端设置`CSRF-TOKEN` Cookie(`SameSite=None; Secure`)。
* 前端读取该Cookie并添加到请求头(如`X-CSRF-Token`)。
* 服务端比对Cookie和请求头的Token是否一致。
##### **高安全组合:JWT + Origin验证 + SameSite Strict**
* **适用场景**:
* **高敏感操作**(如金融交易、密码修改)
* **跨域API服务**(需严格限制来源)
* **实现方式**:
* **JWT(JSON Web Token)** :(详情可看[前端鉴权之JWT-CSDN博客](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2F2301_76603086%2Farticle%2Fdetails%2F147397766%3Fspm%3D1001.2014.3001.5501 "https://blog.csdn.net/2301_76603086/article/details/147397766?spm=1001.2014.3001.5501"))
* 用户登录后签发JWT,包含用户身份和操作权限。
* 请求时通过`Authorization: Bearer <token>`传递。
* **Origin验证**:
* 检查请求头中的`Origin`或`Referer`是否为合法域名。
* **SameSite Strict**:
* 设置关键Cookie为`SameSite=Strict`,完全禁止跨站请求携带。