PHP跨站请求伪造(CSRF)防护实战指南
目录导读
- CSRF攻击的本质与危害 - 理解攻击者如何利用你的身份
- PHP中CSRF漏洞的常见成因 - 为什么你的代码容易中招
- 终极防护方案:Token同步器模式详解 - 最可靠的双重验证机制
- 进阶防护策略:SameSite Cookie与请求头校验 - 纵深防御体系
- 框架级防护(Laravel/ThinkPHP) - 避免重复造轮子
- 常见误区与绕过技巧 - 攻击者的视角
- 安全问答环节 - 开发者高频疑问解答
CSRF攻击的本质与危害
跨站请求伪造(Cross-Site Request Forgery)是一种利用用户已认证身份,在用户不知情的情况下,代其执行非本意操作的攻击手段。

想象这个场景:用户登录了银行网站,cookie未过期,攻击者在论坛发布一张图片,其src指向银行转账接口,用户浏览论坛时,浏览器自动发起对银行接口的HTTP请求,携带着有效的会话cookie,转账指令被执行。整个过程中,攻击者完全不需要获取用户的cookie,只需要一个"诱饵请求"即可。
危害等级:高,可导致资金损失、信息泄露、账号篡改、甚至服务器被控制。
PHP中CSRF漏洞的常见成因
- 仅依赖cookie判断身份 - 没有二次校验请求来源。
- GET请求执行写操作 - 如
/delete.php?id=1,极易被img标签触发。 - 未使用一次性令牌 - 或者令牌可预测(如时间戳)。
- 跨域请求未做Origin/Referer检查。
终极防护方案:Token同步器模式(核心)
这是目前公认最稳固的PHP原生防护手段,核心思想:服务器生成随机令牌,嵌在表单中;提交时校验令牌是否匹配且未过期。
步骤1:生成与存储Token
// 启动会话
session_start();
// 生成新的CSRF令牌(若不存在)
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
步骤2:表单中嵌入隐藏字段
<form method="POST" action="transfer.php">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="text" name="amount">
<button type="submit">转账</button>
</form>
步骤3:服务端严格校验
// 校验请求方法必须是POST(针对关键操作)
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
die('Invalid request method');
}
// 校验令牌是否存在且匹配
if (!isset($_POST['csrf_token']) ||
!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
http_response_code(403);
die('CSRF token validation failed');
}
// 可选:一次性令牌(使用后立即销毁,防止重放)
unset($_SESSION['csrf_token']);
关键细节:使用hash_equals()函数比较字符串,这不是普通,它能防时序攻击,令牌必须用random_bytes()生成,绝不能使用mt_rand()或time()。
进阶防护策略:纵深防御
仅靠Token还不够,建议叠加以下措施:
SameSite Cookie属性(现代浏览器强力防御)
setcookie('session', $sessionId, [
'httponly' => true,
'samesite' => 'Lax', // 或 'Strict'
'secure' => true, // 强制HTTPS
]);
- Lax模式:允许顶层导航GET请求携带cookie,但阻止跨站POST。
- Strict模式:完全阻止跨站携带cookie,但用户体验稍差。
校验Origin/Referer头(低成本辅助)
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowedHost = parse_url(APP_URL, PHP_URL_HOST);
$originHost = parse_url($origin, PHP_URL_HOST);
if ($origin && $originHost !== $allowedHost) {
die('Cross-origin request blocked');
}
注意:Referer可能被浏览器禁用,因此只能作为辅助,不能替代Token。
双重提交Cookie(适用于前后端分离)
在请求头中携带自定义X-CSRF-Token字段,同时该值也存储在cookie中,服务端校验二者是否一致,适用于API接口。
框架级防护(Laravel/ThinkPHP)
主流PHP框架已内置CSRF防护,务必开启:
Laravel
- 全局中间件
VerifyCsrfToken自动验证。 - 在Blade模板中:
@csrf指令自动生成隐藏字段。 - 对AJAX请求:在meta标签中写入token,并通过
fetch或axios携带。
ThinkPHP 6
- 主站默认开启
middleware中的checkCrsf。 - 表单中添加
{:token()}(新版本为{:token_field()})。 - 检查配置文件中
'token_on' => true。
强烈建议:直接使用框架的防护,不要自行实现(除非你非常清楚每个安全边界)。
常见误区与绕过技巧(攻击者视角)
| 常见错误实现 | 攻击者的绕过方式 |
|---|---|
| Token放在URL参数中 | 浏览器历史、Referer泄露 |
| Token不绑定用户会话(全局单token) | 任何用户可互换token |
| 用GET请求提交表单 | img标签、iframe触发 |
| 未检查请求来源 | 跨站图片加载即可触发 |
| 令牌过期时间过长(>30分钟) | 攻击窗口增大 |
对$_REQUEST做校验(包含GET) |
通过GET拼token绕过POST限制 |
经典绕过案例:如果服务器接受$_REQUEST['token']且允许GET发起操作,攻击者构造<img src="transfer.php?amount=999&token=已知token">,此时校验可通过(因为从GET中读到了token)。
安全问答环节
问1:CSRF和XSS有什么区别? 答:XSS是注入恶意脚本到受害页面,从而窃取信息或操纵DOM,CSRF是利用用户已认证的身份,借浏览器发送伪造请求。关键区别:CSRF不依赖注入脚本,只需要诱导受害者发起请求。
问2:我的网站是纯API接口(无页面),还需要CSRF防护吗?
答:需要,API接口更应验证请求来源,推荐方案:要求请求头携带X-Requested-With: XMLHttpRequest,并验证Origin头,但最稳妥是请求头中携带token(非cookie)。
问3:Token存储在哪里更安全?Session还是数据库? 答:Session是最常规做法,数据库存储适合多服务器集群(共享会话),Token本身不敏感,泄露后只要不匹配Session即失效,因此Session存储足够安全。
问4:如何防止CSRF Token被窃取?
答:结合HttpOnly Cookie(防止XSS读取)、Secure属性(仅HTTPS传输)、限制Token有效期(建议每次请求后刷新或5-10分钟后失效)。
CSRF攻击虽然老生常谈,但在现代开发中依然高频出现。核心要点总结:
- 所有写操作必须POST + Token验证
- 使用
random_bytes()生成高熵Token - 用
hash_equals做恒定时间比较 - SameSite Cookie作为第二道防线
- 优先使用成熟框架的防护机制
安全是一条持续对抗的战线,定期审计代码、更新依赖库、保持安全意识,才能守住用户数据的底线,请务必在开发全过程中将CSRF防护内化于设计,而不是事后补救。