PHP项目CSRF攻击如何添加令牌防护

wen PHP项目 27

本文目录导读:

PHP项目CSRF攻击如何添加令牌防护

  1. 生成 CSRF 令牌(通常在 Session 中)
  2. 在表单中添加隐藏字段
  3. 验证 CSRF 令牌(在服务端处理请求时)
  4. 可选:令牌轮换(提高安全性)
  5. 完整封装示例
  6. 重要注意事项

在 PHP 项目中添加 CSRF 令牌防护,通常遵循以下标准流程:

生成 CSRF 令牌(通常在 Session 中)

首先要有一个安全的令牌生成函数:

// 初始化 session
session_start();
// 生成 CSRF 令牌
function generateCsrfToken() {
    // session 中没有令牌,则生成一个新的
    if (empty($_SESSION['csrf_token'])) {
        // 使用 random_bytes 生成安全的随机值
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}
// 获取当前令牌
$csrfToken = generateCsrfToken();

在表单中添加隐藏字段

将令牌嵌入到所有需要保护的 POST/PUT/DELETE 表单中:

<form method="POST" action="/submit.php">
    <!-- 其他表单字段 -->
    <input type="text" name="username">
    <input type="password" name="password">
    <!-- CSRF 令牌隐藏字段 -->
    <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
    <button type="submit">提交</button>
</form>

对于 AJAX 请求,可以在 HTML 的 meta 标签中存储令牌:

<meta name="csrf-token" content="<?php echo htmlspecialchars($csrfToken); ?>">

然后在 JavaScript 中读取并发送:

// 使用 Fetch API
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch('/api/endpoint', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': csrfToken
    },
    body: JSON.stringify({/* 请求体 */})
});
// 或者在 AJAX 中
$.ajax({
    url: '/api/endpoint',
    method: 'POST',
    headers: {
        'X-CSRF-TOKEN': csrfToken
    },
    data: { /* 数据 */ }
});

验证 CSRF 令牌(在服务端处理请求时)

在每个需要保护的后端处理入口进行验证:

// 验证 CSRF 令牌的函数
function validateCsrfToken($token) {
    // 检查 session 中是否存在令牌
    if (!isset($_SESSION['csrf_token'])) {
        return false;
    }
    // 使用 hash_equals 防止时序攻击
    return hash_equals($_SESSION['csrf_token'], $token);
}
// 在 POST 请求处理中
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 获取提交的令牌
    $submittedToken = $_POST['csrf_token'] ?? '';
    // 验证令牌
    if (!validateCsrfToken($submittedToken)) {
        // 令牌无效,终止请求
        http_response_code(403);
        die('CSRF 令牌验证失败,请求被拒绝。');
    }
    // 令牌验证通过,继续处理请求
    // ... 业务逻辑代码
}

可选:令牌轮换(提高安全性)

每次成功验证后可以生成新令牌(防止令牌被反复使用):

function rotateCsrfToken() {
    // 生成新令牌并覆盖旧令牌
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    return $_SESSION['csrf_token'];
}

完整封装示例

创建一个 CSRF 防护类以便复用:

class CsrfProtection {
    public static function init() {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
    }
    public static function getToken() {
        self::init();
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }
    public static function validateToken($token) {
        self::init();
        if (!isset($_SESSION['csrf_token'])) {
            return false;
        }
        return hash_equals($_SESSION['csrf_token'], $token);
    }
    public static function generateTokenField() {
        $token = self::getToken();
        return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token) . '">';
    }
}

使用方式:

// 页面模板中
<!DOCTYPE html>
<html>
<body>
    <form method="POST" action="/process.php">
        <?php echo CsrfProtection::generateTokenField(); ?>
        <input type="text" name="data">
        <input type="submit">
    </form>
</body>
</html>
// 后端处理中
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    if (!CsrfProtection::validateToken($token)) {
        die('CSRF 防护触发');
    }
    // 处理数据...
}

重要注意事项

  1. 永远不要使用 GET 请求修改服务器状态(增删改操作必须使用 POST/PUT/DELETE)
  2. 令牌必须使用安全的随机生成器random_bytesopenssl_random_pseudo_bytes
  3. 比较令牌时一定要使用 hash_equals 防止时序攻击
  4. 令牌应该绑定到用户 session(不同用户不同令牌)
  5. 对于高安全需求,考虑添加 Double Submit Cookie 模式(令牌同时放在 Cookie 和请求体中,服务端对比两者是否一致)

这样基本就能有效防御 CSRF 攻击了。

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