CSRF防护
什么是CSRF攻击
CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种恶意攻击,攻击者诱导用户在已经认证的状态下,向目标网站发送非预期的请求。这种攻击利用了用户在目标网站的认证凭证,执行用户并未意图的操作。
CSRF攻击的危害
- 未经授权的操作:执行用户未授权的操作(如修改密码、转账)
- 数据篡改:修改用户数据或账户设置
- 恶意购买:在电商网站进行未授权购买
- 权限提升:修改用户权限或角色
- 数据泄露:触发敏感数据的导出操作
CSRF攻击的原理
基本攻击流程
- 用户登录目标网站(如银行网站)
- 目标网站返回认证Cookie给用户浏览器
- 用户在未退出登录的情况下,访问恶意网站
- 恶意网站向目标网站发送请求,携带用户的认证Cookie
- 目标网站执行请求,以为是用户的合法操作
攻击示例
<!-- 恶意网站上的代码 -->
<!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应用程序的安全性,防止跨站请求伪造攻击。记住,安全是一个多层次的过程,需要结合多种防护手段。