CSRF防护

什么是CSRF攻击

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种恶意攻击,攻击者诱导用户在已经认证的状态下,向目标网站发送非预期的请求。这种攻击利用了用户在目标网站的认证凭证,执行用户并未意图的操作。

CSRF攻击的危害

  1. 未经授权的操作:执行用户未授权的操作(如修改密码、转账)
  2. 数据篡改:修改用户数据或账户设置
  3. 恶意购买:在电商网站进行未授权购买
  4. 权限提升:修改用户权限或角色
  5. 数据泄露:触发敏感数据的导出操作

CSRF攻击的原理

基本攻击流程

  1. 用户登录目标网站(如银行网站)
  2. 目标网站返回认证Cookie给用户浏览器
  3. 用户在未退出登录的情况下,访问恶意网站
  4. 恶意网站向目标网站发送请求,携带用户的认证Cookie
  5. 目标网站执行请求,以为是用户的合法操作

攻击示例

<!-- 恶意网站上的代码 -->
<!DOCTYPE html>
<html>
<head>
    <title>有趣的图片</title>
</head>
<body>
    <h1>点击查看图片</h1>

    <!-- 方式1:隐藏的表单 -->
    <form id="csrf-form" action="https://bank.example.com/transfer" method="POST" style="display:none;">
        <input type="hidden" name="to_account" value="attacker_account">
        <input type="hidden" name="amount" value="1000">
        <input type="hidden" name="currency" value="USD">
    </form>

    <!-- 方式2:使用图片触发GET请求 -->
    <img src="https://bank.example.com/transfer?to_account=attacker_account&amount=1000"
         style="display:none;" alt="">

    <!-- 方式3:使用JavaScript -->
    <script>
        // 自动提交表单
        window.onload = function() {
            document.getElementById('csrf-form').submit();
        };

        // 或使用XMLHttpRequest/Fetch
        fetch('https://bank.example.com/api/transfer', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                to_account: 'attacker_account',
                amount: 1000
            }),
            credentials: 'include' // 发送Cookie
        });
    </script>
</body>
</html>

CSRF防护方法

1. CSRF令牌(Token)防护

CSRF令牌是最常用和最有效的防护方法。

基本的CSRF令牌实现

<?php
class CsrfProtection {
    private $tokenLength = 32;
    private $sessionKey = '_csrf_token';
    private $tokenExpiry = 3600; // 1小时

    public function __construct() {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
    }

    // 生成CSRF令牌
    public function generateToken() {
        $token = bin2hex(random_bytes($this->tokenLength));

        // 存储令牌到session,包含过期时间
        $_SESSION[$this->sessionKey] = [
            'token' => $token,
            'expires' => time() + $this->tokenExpiry
        ];

        return $token;
    }

    // 验证CSRF令牌
    public function validateToken($token) {
        if (!isset($_SESSION[$this->sessionKey])) {
            return false;
        }

        $storedData = $_SESSION[$this->sessionKey];

        // 检查令牌是否过期
        if (time() > $storedData['expires']) {
            $this->clearToken();
            return false;
        }

        // 验证令牌是否匹配
        $isValid = hash_equals($storedData['token'], $token);

        if ($isValid) {
            // 验证成功后清除令牌(一次性使用)
            $this->clearToken();
        }

        return $isValid;
    }

    // 获取当前令牌
    public function getToken() {
        if (!isset($_SESSION[$this->sessionKey]) ||
            time() > $_SESSION[$this->sessionKey]['expires']) {
            return $this->generateToken();
        }

        return $_SESSION[$this->sessionKey]['token'];
    }

    // 清除令牌
    public function clearToken() {
        unset($_SESSION[$this->sessionKey]);
    }

    // 生成隐藏的CSRF字段HTML
    public function generateHiddenField() {
        $token = $this->getToken();
        return "<input type='hidden' name='csrf_token' value='{$token}'>";
    }

    // 验证请求中的CSRF令牌
    public function validateRequest() {
        $token = $_POST['csrf_token'] ?? $_GET['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;

        if ($token === null) {
            return false;
        }

        return $this->validateToken($token);
    }

    // 为AJAX请求生成令牌
    public function getAjaxToken() {
        return [
            'token' => $this->getToken(),
            'header' => 'X-CSRF-Token'
        ];
    }
}

// 使用示例
$csrf = new CsrfProtection();

// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!$csrf->validateRequest()) {
        die('CSRF令牌验证失败!');
    }

    // 处理表单数据
    echo "表单提交成功!";
}
?>

在表单中使用CSRF令牌

<?php
// 在HTML表单中包含CSRF令牌
function renderForm() {
    $csrf = new CsrfProtection();
    $token = $csrf->getToken();
    ?>
    <!DOCTYPE html>
    <html>
    <head>
        <title>安全表单</title>
    </head>
    <body>
        <form action="process.php" method="POST">
            <?php echo $csrf->generateHiddenField(); ?>

            <div>
                <label for="username">用户名:</label>
                <input type="text" id="username" name="username" required>
            </div>

            <div>
                <label for="email">邮箱:</label>
                <input type="email" id="email" name="email" required>
            </div>

            <div>
                <label for="message">消息:</label>
                <textarea id="message" name="message" required></textarea>
            </div>

            <button type="submit">提交</button>
        </form>
    </body>
    </html>
    <?php
}

// 处理表单提交
function processForm() {
    $csrf = new CsrfProtection();

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if (!$csrf->validateRequest()) {
            http_response_code(403);
            echo "请求被拒绝:CSRF令牌无效";
            return;
        }

        // 验证表单数据
        $username = filter_input(INPUT_POST, 'username', FILTER_SANITIZE_STRING);
        $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
        $message = filter_input(INPUT_POST, 'message', FILTER_SANITIZE_STRING);

        if ($username && $email && $message) {
            // 处理数据(保存到数据库等)
            echo "表单提交成功!";
        } else {
            echo "请填写所有必填字段";
        }
    }
}
?>

2. SameSite Cookie属性

SameSite属性可以防止Cookie在跨站请求中发送。

<?php
class SecureSession {
    // 启用SameSite Cookie
    public static function startSecureSession($lifetime = 0, $path = '/', $domain = '') {
        // 设置Cookie参数,包括SameSite
        $cookieParams = session_get_cookie_params();

        session_set_cookie_params([
            'lifetime' => $lifetime ?: $cookieParams['lifetime'],
            'path' => $path ?: $cookieParams['path'],
            'domain' => $domain ?: $cookieParams['domain'],
            'secure' => true,  // 仅通过HTTPS传输
            'httponly' => true, // 禁止JavaScript访问
            'samesite' => 'Strict' // 或 'Lax'
        ]);

        session_start();

        // 重新生成会话ID
        session_regenerate_id(true);
    }

    // 设置安全的会话Cookie
    public static function setSecureCookie($name, $value, $expire = 0, $path = '/', $domain = '') {
        $options = [
            'expires' => $expire,
            'path' => $path,
            'domain' => $domain,
            'secure' => true,
            'httponly' => true,
            'samesite' => 'Lax'
        ];

        setcookie($name, $value, $options);
    }
}

// 在应用开始时启用安全会话
SecureSession::startSecureSession();
?>

3. 验证Referer和Origin头部

<?php
class RequestValidator {
    private $allowedDomains;
    private $allowSubdomains;

    public function __construct($allowedDomains = [], $allowSubdomains = false) {
        $this->allowedDomains = $allowedDomains;
        $this->allowSubdomains = $allowSubdomains;
    }

    // 验证请求来源
    public function validateOrigin() {
        $origin = $_SERVER['HTTP_ORIGIN'] ?? $_SERVER['HTTP_REFERER'] ?? null;

        if ($origin === null) {
            // 某些请求可能不包含Origin头
            return true;
        }

        return $this->isAllowedDomain($origin);
    }

    // 检查是否为允许的域名
    private function isAllowedDomain($url) {
        $parsedUrl = parse_url($url);
        $host = $parsedUrl['host'] ?? '';

        foreach ($this->allowedDomains as $allowedDomain) {
            if ($this->allowSubdomains) {
                // 允许子域名
                if ($host === $allowedDomain || str_ends_with($host, '.' . $allowedDomain)) {
                    return true;
                }
            } else {
                // 不允许子域名
                if ($host === $allowedDomain) {
                    return true;
                }
            }
        }

        return false;
    }

    // 验证AJAX请求
    public function validateAjaxRequest() {
        if (!$this->isAjaxRequest()) {
            return false;
        }

        return $this->validateOrigin();
    }

    // 检查是否为AJAX请求
    private function isAjaxRequest() {
        return (
            isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
            strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'
        ) || (
            isset($_SERVER['HTTP_ACCEPT']) &&
            strpos(strtolower($_SERVER['HTTP_ACCEPT']), 'application/json') !== false
        );
    }
}

// 使用示例
$validator = new RequestValidator(
    ['example.com', 'www.example.com'],
    true // 允许子域名
);

// 在处理请求前验证
if (!$validator->validateOrigin()) {
    http_response_code(403);
    echo "请求被拒绝:无效的来源";
    exit;
}
?>

4. 双重提交Cookie

双重提交Cookie是一种额外的防护措施。

<?php
class DoubleSubmitCookie {
    private $cookieName = 'csrf_cookie';
    private $paramName = 'csrf_param';
    private $tokenLength = 32;

    public function generateToken() {
        return bin2hex(random_bytes($this->tokenLength));
    }

    // 设置CSRF Cookie
    public function setCookie() {
        $token = $this->generateToken();

        // 设置Cookie,HttpOnly设为false以便JavaScript可以读取
        setcookie(
            $this->cookieName,
            $token,
            [
                'expires' => 0,
                'path' => '/',
                'domain' => '',
                'secure' => true,
                'httponly' => false,
                'samesite' => 'Strict'
            ]
        );

        return $token;
    }

    // 验证双重提交
    public function validate() {
        $cookieToken = $_COOKIE[$this->cookieName] ?? null;
        $paramToken = $_POST[$this->paramName] ?? $_GET[$this->paramName] ?? null;

        if ($cookieToken === null || $paramToken === null) {
            return false;
        }

        return hash_equals($cookieToken, $paramToken);
    }

    // 获取令牌(用于表单)
    public function getToken() {
        $token = $_COOKIE[$this->cookieName] ?? $this->setCookie();
        return $token;
    }

    // 生成隐藏字段
    public function generateHiddenField() {
        $token = $this->getToken();
        return "<input type='hidden' name='{$this->paramName}' value='{$token}'>";
    }
}
?>

5. 完整的CSRF防护类

<?php
class CsrfGuard {
    private $tokenLength = 32;
    private $sessionKey = '_csrf_tokens';
    private $tokenExpiry = 3600;
    private $maxTokens = 10;

    public function __construct() {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        // 初始化令牌存储
        if (!isset($_SESSION[$this->sessionKey])) {
            $_SESSION[$this->sessionKey] = [];
        }

        // 清理过期令牌
        $this->cleanupExpiredTokens();
    }

    // 生成新的CSRF令牌
    public function generateToken($action = 'default') {
        $token = bin2hex(random_bytes($this->tokenLength));

        $_SESSION[$this->sessionKey][$action] = [
            'token' => $token,
            'created' => time(),
            'used' => false
        ];

        // 限制令牌数量
        if (count($_SESSION[$this->sessionKey]) > $this->maxTokens) {
            $oldestAction = array_key_first($_SESSION[$this->sessionKey]);
            unset($_SESSION[$this->sessionKey][$oldestAction]);
        }

        return $token;
    }

    // 验证令牌
    public function validateToken($token, $action = 'default') {
        if (!isset($_SESSION[$this->sessionKey][$action])) {
            return false;
        }

        $tokenData = $_SESSION[$this->sessionKey][$action];

        // 检查令牌是否过期
        if (time() - $tokenData['created'] > $this->tokenExpiry) {
            unset($_SESSION[$this->sessionKey][$action]);
            return false;
        }

        // 检查令牌是否已使用(一次性令牌)
        if ($tokenData['used']) {
            return false;
        }

        // 验证令牌
        $isValid = hash_equals($tokenData['token'], $token);

        if ($isValid) {
            // 标记为已使用
            $_SESSION[$this->sessionKey][$action]['used'] = true;
        }

        return $isValid;
    }

    // 自动验证请求
    public function validateRequest($action = 'default') {
        $token = $this->getTokenFromRequest();

        if ($token === null) {
            return false;
        }

        return $this->validateToken($token, $action);
    }

    // 从请求中获取令牌
    private function getTokenFromRequest() {
        // 从POST数据获取
        if (isset($_POST['_csrf_token'])) {
            return $_POST['_csrf_token'];
        }

        // 从GET参数获取
        if (isset($_GET['_csrf_token'])) {
            return $_GET['_csrf_token'];
        }

        // 从HTTP头部获取
        if (isset($_SERVER['HTTP_X_CSRF_TOKEN'])) {
            return $_SERVER['HTTP_X_CSRF_TOKEN'];
        }

        return null;
    }

    // 清理过期令牌
    private function cleanupExpiredTokens() {
        foreach ($_SESSION[$this->sessionKey] as $action => $tokenData) {
            if (time() - $tokenData['created'] > $this->tokenExpiry) {
                unset($_SESSION[$this->sessionKey][$action]);
            }
        }
    }

    // 生成隐藏字段
    public function generateHiddenField($action = 'default') {
        $token = $this->generateToken($action);
        return "<input type='hidden' name='_csrf_token' value='{$token}'>";
    }

    // 获取AJAX令牌
    public function getAjaxToken($action = 'default') {
        return [
            'name' => '_csrf_token',
            'value' => $this->generateToken($action),
            'header' => 'X-CSRF-Token'
        ];
    }

    // 注入CSRF元标签到HTML头部
    public function injectMetaTag($action = 'default') {
        $token = $this->generateToken($action);
        echo "<meta name='csrf-token' content='{$token}'>";
    }
}

// 中间件示例
class CsrfMiddleware {
    private $csrfGuard;
    private $excludedRoutes;

    public function __construct($excludedRoutes = []) {
        $this->csrfGuard = new CsrfGuard();
        $this->excludedRoutes = $excludedRoutes;
    }

    // 处理请求
    public function handle($route, $callback) {
        // 检查是否为排除的路由
        if ($this->isExcludedRoute($route)) {
            return $callback();
        }

        // 验证CSRF令牌
        if ($_SERVER['REQUEST_METHOD'] !== 'GET' && !$this->csrfGuard->validateRequest()) {
            http_response_code(403);
            echo json_encode(['error' => 'CSRF token validation failed']);
            exit;
        }

        return $callback();
    }

    // 检查是否为排除的路由
    private function isExcludedRoute($route) {
        foreach ($this->excludedRoutes as $excludedRoute) {
            if (fnmatch($excludedRoute, $route)) {
                return true;
            }
        }
        return false;
    }
}
?>

6. 前端JavaScript集成

// CSRF令牌管理
class CsrfManager {
    constructor() {
        this.token = this.getToken();
        this.setupAjax();
    }

    // 获取CSRF令牌
    getToken() {
        const meta = document.querySelector('meta[name="csrf-token"]');
        if (meta) {
            return meta.getAttribute('content');
        }

        // 从隐藏字段获取
        const hidden = document.querySelector('input[name="_csrf_token"]');
        if (hidden) {
            return hidden.value;
        }

        return null;
    }

    // 为所有AJAX请求添加CSRF令牌
    setupAjax() {
        if (!this.token) return;

        // Fetch API拦截器
        const originalFetch = window.fetch;
        window.fetch = function(url, options = {}) {
            if (options.method && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(options.method.toUpperCase())) {
                options.headers = {
                    ...options.headers,
                    'X-CSRF-Token': window.csrfManager.token
                };
            }
            return originalFetch.apply(this, arguments);
        };

        // jQuery AJAX设置
        if (typeof $ !== 'undefined') {
            $.ajaxSetup({
                beforeSend: function(xhr, settings) {
                    if (settings.type && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(settings.type.toUpperCase())) {
                        xhr.setRequestHeader('X-CSRF-Token', window.csrfManager.token);
                    }
                }
            });
        }

        // Axios拦截器
        if (typeof axios !== 'undefined') {
            axios.defaults.headers.common['X-CSRF-Token'] = this.token;
        }
    }

    // 动态更新表单令牌
    updateFormTokens() {
        const forms = document.querySelectorAll('form');
        forms.forEach(form => {
            let tokenField = form.querySelector('input[name="_csrf_token"]');
            if (!tokenField) {
                tokenField = document.createElement('input');
                tokenField.type = 'hidden';
                tokenField.name = '_csrf_token';
                form.appendChild(tokenField);
            }
            tokenField.value = this.token;
        });
    }

    // 刷新令牌
    async refreshToken() {
        try {
            const response = await fetch('/csrf-token', {
                method: 'POST',
                headers: {
                    'X-CSRF-Token': this.token
                }
            });
            const data = await response.json();
            this.token = data.token;
            this.updateFormTokens();
        } catch (error) {
            console.error('Failed to refresh CSRF token:', error);
        }
    }
}

// 初始化CSRF管理器
document.addEventListener('DOMContentLoaded', function() {
    window.csrfManager = new CsrfManager();
});

7. 实际应用示例

<?php
// 完整的Web应用CSRF防护示例
class SecureWebApp {
    private $csrfGuard;
    private $db;

    public function __construct() {
        $this->csrfGuard = new CsrfGuard();
        // 初始化数据库连接
        $this->db = new Database('localhost', 'user', 'pass', 'app');
    }

    // 处理用户注册
    public function handleRegister() {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            if (!$this->csrfGuard->validateRequest('register')) {
                $this->jsonResponse(['error' => 'CSRF验证失败'], 403);
                return;
            }

            $username = filter_input(INPUT_POST, 'username', FILTER_SANITIZE_STRING);
            $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
            $password = $_POST['password'];

            // 验证输入
            if (!$username || !$email || !$password) {
                $this->jsonResponse(['error' => '请填写所有字段'], 400);
                return;
            }

            // 检查用户是否已存在
            if ($this->db->userExists($email)) {
                $this->jsonResponse(['error' => '邮箱已被注册'], 409);
                return;
            }

            // 创建用户
            $userId = $this->db->createUser($username, $email, $password);
            if ($userId) {
                $this->jsonResponse(['success' => '注册成功', 'user_id' => $userId]);
            } else {
                $this->jsonResponse(['error' => '注册失败'], 500);
            }
        }
    }

    // 处理密码修改
    public function handleChangePassword() {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            if (!$this->csrfGuard->validateRequest('change_password')) {
                $this->jsonResponse(['error' => 'CSRF验证失败'], 403);
                return;
            }

            $currentPassword = $_POST['current_password'];
            $newPassword = $_POST['new_password'];
            $confirmPassword = $_POST['confirm_password'];

            // 验证密码
            if ($newPassword !== $confirmPassword) {
                $this->jsonResponse(['error' => '新密码不匹配'], 400);
                return;
            }

            if (strlen($newPassword) < 8) {
                $this->jsonResponse(['error' => '密码至少8个字符'], 400);
                return;
            }

            // 验证当前密码
            $userId = $_SESSION['user_id'];
            if (!$this->db->verifyPassword($userId, $currentPassword)) {
                $this->jsonResponse(['error' => '当前密码错误'], 401);
                return;
            }

            // 更新密码
            if ($this->db->updatePassword($userId, $newPassword)) {
                $this->jsonResponse(['success' => '密码修改成功']);
            } else {
                $this->jsonResponse(['error' => '密码修改失败'], 500);
            }
        }
    }

    // 渲染表单
    public function renderForm($formType) {
        ?>
        <!DOCTYPE html>
        <html>
        <head>
            <title>安全表单</title>
            <?php $this->csrfGuard->injectMetaTag($formType); ?>
            <script src="csrf-manager.js"></script>
        </head>
        <body>
            <?php if ($formType === 'register'): ?>
                <form id="register-form" method="POST" action="register.php">
                    <?php echo $this->csrfGuard->generateHiddenField('register'); ?>
                    <h2>用户注册</h2>
                    <div>
                        <label>用户名:</label>
                        <input type="text" name="username" required>
                    </div>
                    <div>
                        <label>邮箱:</label>
                        <input type="email" name="email" required>
                    </div>
                    <div>
                        <label>密码:</label>
                        <input type="password" name="password" required>
                    </div>
                    <button type="submit">注册</button>
                </form>
            <?php elseif ($formType === 'change_password'): ?>
                <form id="password-form" method="POST" action="change-password.php">
                    <?php echo $this->csrfGuard->generateHiddenField('change_password'); ?>
                    <h2>修改密码</h2>
                    <div>
                        <label>当前密码:</label>
                        <input type="password" name="current_password" required>
                    </div>
                    <div>
                        <label>新密码:</label>
                        <input type="password" name="new_password" required>
                    </div>
                    <div>
                        <label>确认新密码:</label>
                        <input type="password" name="confirm_password" required>
                    </div>
                    <button type="submit">修改密码</button>
                </form>
            <?php endif; ?>
        </body>
        </html>
        <?php
    }

    // JSON响应
    private function jsonResponse($data, $statusCode = 200) {
        http_response_code($statusCode);
        header('Content-Type: application/json');
        echo json_encode($data);
        exit;
    }
}

// 路由处理
$app = new SecureWebApp();

// 中间件
$middleware = new CsrfMiddleware([
    'csrf-token' // 排除CSRF令牌获取路由
]);

// 路由定义
$routes = [
    'GET /register' => function() use ($app) {
        $app->renderForm('register');
    },
    'POST /register' => function() use ($app) {
        $middleware->handle('/register', function() use ($app) {
            $app->handleRegister();
        });
    },
    'POST /change-password' => function() use ($app) {
        $middleware->handle('/change-password', function() use ($app) {
            $app->handleChangePassword();
        });
    },
    'POST /csrf-token' => function() use ($app) {
        // 获取新的CSRF令牌
        $token = $app->csrfGuard->generateToken();
        header('Content-Type: application/json');
        echo json_encode(['token' => $token]);
    }
];

// 简单的路由分发
$requestMethod = $_SERVER['REQUEST_METHOD'];
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$routeKey = "$requestMethod $requestUri";

if (isset($routes[$routeKey])) {
    $routes[$routeKey]();
} else {
    http_response_code(404);
    echo "页面未找到";
}
?>

检测CSRF漏洞

CSRF漏洞扫描器

<?php
class CsrfScanner {
    private $targetUrl;
    private $userAgent = 'CSRF Scanner';

    public function __construct($targetUrl) {
        $this->targetUrl = $targetUrl;
    }

    // 扫描CSRF漏洞
    public function scan() {
        $report = [
            'target' => $this->targetUrl,
            'scan_date' => date('Y-m-d H:i:s'),
            'vulnerabilities' => []
        ];

        // 检查表单
        $forms = $this->extractForms();
        foreach ($forms as $form) {
            $vulnerability = $this->analyzeForm($form);
            if ($vulnerability) {
                $report['vulnerabilities'][] = $vulnerability;
            }
        }

        // 检查AJAX端点
        $ajaxEndpoints = $this->findAjaxEndpoints();
        foreach ($ajaxEndpoints as $endpoint) {
            $vulnerability = $this->analyzeAjaxEndpoint($endpoint);
            if ($vulnerability) {
                $report['vulnerabilities'][] = $vulnerability;
            }
        }

        return $report;
    }

    // 提取表单
    private function extractForms() {
        $html = $this->fetchPage($this->targetUrl);
        if (!$html) {
            return [];
        }

        $dom = new DOMDocument();
        @$dom->loadHTML($html);
        $forms = [];

        foreach ($dom->getElementsByTagName('form') as $form) {
            $formInfo = [
                'action' => $form->getAttribute('action'),
                'method' => strtoupper($form->getAttribute('method') ?: 'GET'),
                'has_csrf_token' => false,
                'csrf_field_name' => null
            ];

            // 检查CSRF令牌字段
            foreach ($form->getElementsByTagName('input') as $input) {
                $name = $input->getAttribute('name');
                if (preg_match('/csrf|token/i', $name)) {
                    $formInfo['has_csrf_token'] = true;
                    $formInfo['csrf_field_name'] = $name;
                    break;
                }
            }

            $forms[] = $formInfo;
        }

        return $forms;
    }

    // 分析表单漏洞
    private function analyzeForm($form) {
        // 如果是GET请求,CSRF风险较低
        if ($form['method'] === 'GET') {
            return null;
        }

        // 检查是否有CSRF令牌
        if ($form['has_csrf_token']) {
            return null;
        }

        // 检查是否为敏感操作
        $action = strtolower($form['action']);
        $sensitiveKeywords = ['delete', 'update', 'change', 'password', 'email', 'transfer', 'purchase'];

        foreach ($sensitiveKeywords as $keyword) {
            if (strpos($action, $keyword) !== false) {
                return [
                    'type' => 'CSRF in Form',
                    'action' => $form['action'],
                    'method' => $form['method'],
                    'severity' => 'High',
                    'description' => '表单缺少CSRF令牌保护,可能执行敏感操作'
                ];
            }
        }

        return [
            'type' => 'CSRF in Form',
            'action' => $form['action'],
            'method' => $form['method'],
            'severity' => 'Medium',
            'description' => '表单缺少CSRF令牌保护'
        ];
    }

    // 查找AJAX端点
    private function findAjaxEndpoints() {
        $html = $this->fetchPage($this->targetUrl);
        if (!$html) {
            return [];
        }

        $endpoints = [];

        // 查找JavaScript中的API调用
        if (preg_match_all('/(?:fetch|ajax|get|post)\s*\(\s*[\'"]([^\'"]+)[\'"]/', $html, $matches)) {
            foreach ($matches[1] as $url) {
                if (strpos($url, 'http') !== 0) {
                    $url = rtrim($this->targetUrl, '/') . '/' . ltrim($url, '/');
                }
                $endpoints[] = $url;
            }
        }

        return array_unique($endpoints);
    }

    // 分析AJAX端点
    private function analyzeAjaxEndpoint($endpoint) {
        // 这里可以发送测试请求来检查CSRF保护
        // 简化示例:返回潜在风险
        return [
            'type' => 'Potential CSRF in AJAX',
            'endpoint' => $endpoint,
            'severity' => 'Info',
            'description' => '需要手动检查此AJAX端点的CSRF保护'
        ];
    }

    // 获取页面内容
    private function fetchPage($url) {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_USERAGENT => $this->userAgent,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_TIMEOUT => 30
        ]);

        $content = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        return ($httpCode === 200) ? $content : false;
    }
}

// 使用示例
$scanner = new CsrfScanner('http://example.com');
$report = $scanner->scan();

echo "CSRF扫描报告:\n";
echo "目标: {$report['target']}\n";
echo "扫描时间: {$report['scan_date']}\n";
echo "发现漏洞: " . count($report['vulnerabilities']) . "\n\n";

foreach ($report['vulnerabilities'] as $vuln) {
    echo "类型: {$vuln['type']}\n";
    echo "严重程度: {$vuln['severity']}\n";
    echo "描述: {$vuln['description']}\n";
    if (isset($vuln['action'])) {
        echo "动作: {$vuln['action']}\n";
    }
    echo "\n";
}
?>

最佳实践总结

1. 多层防护策略

<?php
// 综合防护示例
class SecureApplication {
    private $csrfGuard;
    private $requestValidator;

    public function __construct() {
        // 启动安全会话
        SecureSession::startSecureSession();

        // 初始化CSRF保护
        $this->csrfGuard = new CsrfGuard();

        // 初始化请求验证
        $this->requestValidator = new RequestValidator(
            ['example.com'],
            true
        );

        // 设置安全头部
        SecurityHeaders::setAll();
    }

    public function handleRequest() {
        // 1. 验证请求来源
        if (!$this->requestValidator->validateOrigin()) {
            http_response_code(403);
            die('请求来源无效');
        }

        // 2. 对于状态改变操作,验证CSRF令牌
        if ($this->isStateChangingRequest()) {
            if (!$this->csrfGuard->validateRequest()) {
                http_response_code(403);
                die('CSRF令牌无效');
            }
        }

        // 3. 处理请求
        $this->processRequest();
    }

    private function isStateChangingRequest() {
        $method = $_SERVER['REQUEST_METHOD'];
        return in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH']);
    }
}
?>

2. 关键操作的特殊保护

<?php
// 对敏感操作实施额外保护
class SensitiveOperationProtection {
    // 要求重新认证
    public function requireReauthentication() {
        $lastAuth = $_SESSION['last_auth_time'] ?? 0;
        $maxAge = 300; // 5分钟

        if (time() - $lastAuth > $maxAge) {
            // 重定向到重新认证页面
            header('Location: /reauthenticate?return=' . urlencode($_SERVER['REQUEST_URI']));
            exit;
        }
    }

    // 操作确认
    public function requireConfirmation($operation, $data) {
        $_SESSION['pending_operation'] = [
            'operation' => $operation,
            'data' => $data,
            'expires' => time() + 600 // 10分钟有效期
        ];

        // 显示确认页面
        include 'confirm_operation.php';
        exit;
    }

    // 执行已确认的操作
    public function executeConfirmedOperation() {
        if (!isset($_SESSION['pending_operation'])) {
            throw new Exception('没有待处理的操作');
        }

        $operation = $_SESSION['pending_operation'];

        if (time() > $operation['expires']) {
            unset($_SESSION['pending_operation']);
            throw new Exception('操作已过期');
        }

        // 执行操作
        $result = $this->performOperation($operation['operation'], $operation['data']);

        // 清除待处理操作
        unset($_SESSION['pending_operation']);

        return $result;
    }
}
?>

3. 定期安全审计

<?php
// CSRF保护审计
class CsrfAuditor {
    private $reportFile = 'csrf_audit.log';

    public function audit() {
        $report = [
            'timestamp' => date('Y-m-d H:i:s'),
            'checks' => []
        ];

        // 检查会话配置
        $report['checks']['session'] = $this->checkSessionConfig();

        // 检查CSRF令牌实现
        $report['checks']['csrf_tokens'] = $this->checkCsrfImplementation();

        // 检查表单
        $report['checks']['forms'] = $this->checkForms();

        // 记录报告
        $this->saveReport($report);

        return $report;
    }

    private function checkSessionConfig() {
        $config = session_get_cookie_params();

        return [
            'secure' => $config['secure'] ?? false,
            'httponly' => $config['httponly'] ?? false,
            'samesite' => $config['samesite'] ?? 'None',
            'passed' => ($config['secure'] ?? false) &&
                      ($config['httponly'] ?? false) &&
                      in_array($config['samesite'] ?? '', ['Lax', 'Strict'])
        ];
    }

    private function checkCsrfImplementation() {
        // 检查是否有CSRF保护类
        $hasCsrfClass = class_exists('CsrfGuard') || class_exists('CsrfProtection');

        return [
            'has_csrf_class' => $hasCsrfClass,
            'passed' => $hasCsrfClass
        ];
    }

    private function checkForms() {
        $forms = glob('*.php');
        $vulnerableForms = [];

        foreach ($forms as $file) {
            $content = file_get_contents($file);

            // 查找POST表单
            if (preg_match_all('/<form[^>]*method=["\']post["\'][^>]*>/i', $content, $matches)) {
                foreach ($matches[0] as $form) {
                    if (!preg_match('/csrf|token/i', $form)) {
                        $vulnerableForms[] = $file;
                        break;
                    }
                }
            }
        }

        return [
            'total_forms' => count($forms),
            'vulnerable_forms' => $vulnerableForms,
            'passed' => empty($vulnerableForms)
        ];
    }

    private function saveReport($report) {
        $logEntry = json_encode($report) . "\n";
        file_put_contents($this->reportFile, $logEntry, FILE_APPEND);
    }
}

// 定期运行审计
$schedule = new CsrfAuditor();
$report = $schedule->audit();

// 发送告警(如果有问题)
if (!$report['checks']['session']['passed'] ||
    !$report['checks']['csrf_tokens']['passed'] ||
    !$report['checks']['forms']['passed']) {
    // 发送邮件或通知管理员
    error_log('CSRF保护审计发现问题!');
}
?>

通过实施这些CSRF防护措施,你可以大大提高Web应用程序的安全性,防止跨站请求伪造攻击。记住,安全是一个多层次的过程,需要结合多种防护手段。