PHP项目CSRF攻击如何有效防御

wen PHP项目 4

本文目录导读:

PHP项目CSRF攻击如何有效防御

  1. 第一步:核心防御(Token验证)
  2. 第二步:进阶加固(针对特定情境)
  3. 第三步:全局防护策略(彻底清除漏洞)
  4. 第四步:完整代码示例(封装类)
  5. 总结:必须避开的“伪防御”

在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'];
}
?>

嵌入表单(隐藏字段)

在所有使用 POSTPUTDELETE 方法的表单中,加入隐藏字段。

<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 配合使用。

  1. 同源检测(Origin/Sec-Fetch-Site Header): 检查请求头中的 OriginReferer,确保它来自你自己的域名,现代浏览器会发送 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');
    }
  2. 设置 Cookie 的 SameSite 属性: 在 PHP 7.3+ 中,你可以设置 SameSite 属性,设置为 StrictLax 可以阻止浏览器在跨站请求时携带 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();
    // 业务逻辑...
}
?>

必须避开的“伪防御”

  1. 不要只用 Referer 检查:某些浏览器或代理会禁用 Referer,导致误杀或绕过。
  2. 不要只用 $_SERVER['HTTP_ORIGIN']:老浏览器可能不发送该头。
  3. 不要只用 GET 请求:绝不使用 GET 请求执行修改数据(增删改)的操作。
  4. Token 不要使用 md5(uniqid()):必须使用 random_bytesopenssl_random_pseudo_bytes 等加密安全的伪随机数生成器。

最佳实践组合SameSite=Lax 的 Cookie + 每次会话固定的强随机 Token(嵌入表单)+ 服务端 hash_equals 严格比对 + 对敏感操作(如支付、改密码)要求二次验证(如输入当前密码)。

抱歉,评论功能暂时关闭!