本文目录导读:

在PHP项目中防御CSRF攻击,核心思路是确保请求是用户主动发起的,而非被第三方网站伪造的。
最有效且通用的防御方法是同步器令牌模式(Synchronizer Token Pattern),即:在表单中嵌入一个随机生成的Token,并在服务器端验证。
以下是完整的防御方案,分为基础实现、进阶加固和代码示例。
第一步:核心防御(Token验证)
这是必须实现的基础,分为三部分:生成、嵌入、验证。
生成 Token(会话级)
在每个用户会话(Session)中生成一个唯一的随机Token。
<?php
// session_start() 务必在输出任何内容之前调用
session_start();
// 如果Token不存在,则生成一个新的
if (empty($_SESSION['csrf_token'])) {
// bin2hex + random_bytes 是生成强随机数的最佳实践
// 不要使用 rand() 或 mt_rand()
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// 获取Token的辅助函数
function getCsrfToken() {
return $_SESSION['csrf_token'];
}
?>
嵌入表单(隐藏字段)
在所有使用 POST、PUT、DELETE 方法的表单中,加入隐藏字段。
<form method="POST" action="/submit.php">
<!-- 核心:隐藏字段携带Token -->
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(getCsrfToken()); ?>">
<input type="text" name="username" required>
<button type="submit">提交</button>
</form>
验证 Token(服务端)
在处理请求的 PHP 文件顶部,校验提交的Token是否与会话中的一致。
<?php
session_start();
// 处理POST请求时进行验证
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. 检查是否提交了token
if (!isset($_POST['csrf_token'])) {
die('CSRF token missing.');
}
// 2. 对比提交的token与会话中的token
// 使用 hash_equals 防止时序攻击(Timing Attack)
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
// 验证失败:记录日志、跳转错误页或直接终止
http_response_code(403);
die('CSRF token validation failed.');
}
// 3. 验证通过,继续处理业务逻辑...
// 处理你的表单数据
}
?>
第二步:进阶加固(针对特定情境)
如果你的项目使用AJAX(异步请求)或API接口,需要注意以下两点:
AJAX 请求处理
如果是通过 JavaScript 发送请求,不能依赖表单字段,建议在 HTTP 请求头 中携带 Token。
前端(jQuery示例):
$.ajaxSetup({
headers: {
'X-CSRF-Token': '<?php echo getCsrfToken(); ?>'
}
});
后端(PHP验证):
// 获取请求头中的Token
$headers = getallheaders();
$token = $headers['X-CSRF-Token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'], $token)) {
http_response_code(403);
die('Invalid CSRF token');
}
双重提交Cookie(Double Submit Cookie)
此方法不需要存储 Session(适用于无状态API),服务器不存储Token,而是在Cookie中设置一个随机值,同时要求请求参数或请求头中也包含相同的值,服务器比对两者是否一致。
// 生成时:设置Cookie并返回给前端
$token = bin2hex(random_bytes(32));
setcookie('csrf_cookie', $token, time() + 3600, '/', '', true, true); // Secure + HttpOnly
// 验证时:比对Cookie值和请求头/参数值
if ($_COOKIE['csrf_cookie'] !== ($_POST['csrf_token'] ?? '')) {
die('Invalid CSRF token');
}
第三步:全局防护策略(彻底清除漏洞)
注意: 以下措施是补充手段,不能单独作为防御屏障,需与 Token 配合使用。
-
同源检测(Origin/Sec-Fetch-Site Header): 检查请求头中的
Origin或Referer,确保它来自你自己的域名,现代浏览器会发送Sec-Fetch-Site头,值为same-origin时安全。// 简单检查Origin $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $allowed = ['https://yourdomain.com', 'https://www.yourdomain.com']; if (!in_array($origin, $allowed)) { die('Invalid Origin'); } -
设置 Cookie 的 SameSite 属性: 在 PHP 7.3+ 中,你可以设置
SameSite属性,设置为Strict或Lax可以阻止浏览器在跨站请求时携带 Session Cookie,这能从根源上阻断大量CSRF攻击。// 设置 Session Cookie 的 SameSite 属性 session_set_cookie_params([ 'lifetime' => 0, 'path' => '/', 'httponly' => true, 'secure' => true, // 仅HTTPS 'samesite' => 'Lax' // 或 'Strict' ]); session_start();
第四步:完整代码示例(封装类)
为了方便复用,推荐封装成一个工具类。
<?php
class CSRF {
// 1. 开启会话并初始化Token
public static function init() {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
}
// 2. 获取Token(用于表单隐藏域)
public static function getToken() {
self::init();
return $_SESSION['csrf_token'];
}
// 3. 生成表单隐藏字段的HTML
public static function field() {
self::init();
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($_SESSION['csrf_token']) . '">';
}
// 4. 验证请求(POST参数或Header)
public static function validate() {
self::init();
// 从 POST 或 Header 中获取提交的token
$submittedToken = $_POST['csrf_token'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
if (empty($submittedToken) || !hash_equals($_SESSION['csrf_token'], $submittedToken)) {
// 可以记录日志:error_log('CSRF Violation: ' . $_SERVER['REMOTE_ADDR']);
http_response_code(403);
exit('CSRF validation failed.');
}
return true;
}
}
// --- 使用方式 ---
// common.php 或 页面顶部调用初始化
CSRF::init();
// 表单中输出: <?php echo CSRF::field(); ?>
// 提交处理页面顶部:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
CSRF::validate();
// 业务逻辑...
}
?>
必须避开的“伪防御”
- 不要只用 Referer 检查:某些浏览器或代理会禁用 Referer,导致误杀或绕过。
- 不要只用
$_SERVER['HTTP_ORIGIN']:老浏览器可能不发送该头。 - 不要只用 GET 请求:绝不使用 GET 请求执行修改数据(增删改)的操作。
- Token 不要使用
md5(uniqid()):必须使用random_bytes或openssl_random_pseudo_bytes等加密安全的伪随机数生成器。
最佳实践组合:SameSite=Lax 的 Cookie + 每次会话固定的强随机 Token(嵌入表单)+ 服务端 hash_equals 严格比对 + 对敏感操作(如支付、改密码)要求二次验证(如输入当前密码)。