CSRF 攻击防范策略

2025-08-22 20:05:00
丁国栋
原创 10
摘要:本文详细介绍 CSRF 相关知识和PHP编程语言实现。

CSRF 攻击防范策略

什么是 CSRF 攻击?

一、核心概念:什么是 CSRF?

CSRF(Cross-Site Request Forgery),中文翻译为“跨站请求伪造”。它是一种常见的网络攻击方式。

你可以用一个简单的比喻来理解它:

“冒名顶替”“借刀杀人”

攻击者欺骗你的浏览器,让你在不知情的情况下,使用你亲自操作过的一个已认证用户的身份,向一个你原本信任的网站(比如你的网上银行、社交媒体或邮箱)发送一个伪造的请求,执行了某些操作(比如转账、改密码、发状态)。

关键点在于:攻击者利用的是你已经在该网站登录后的身份认证凭证(通常是浏览器 Cookie)。因为浏览器会自动在每次请求中带上对应网站的 Cookie,所以目标网站会认为这个请求是你本人自愿发出的。


二、CSRF 攻击是如何发生的?

一个典型的 CSRF 攻击需要同时满足以下几个条件:

  1. 用户已登录信任网站:用户已经登录了目标网站(如 bank.com),并在浏览器中保存了登录凭证(Session Cookie)。
  2. 用户访问恶意网站:用户随后在同一个浏览器中,访问了攻击者精心构造的恶意网站或链接。
  3. 恶意请求被触发:这个恶意网站包含了一个会自动向目标网站(bank.com)发送请求的代码(比如一个隐藏的表单或一个 <img> 标签)。
  4. 请求携带用户凭证:由于用户已登录,浏览器在发送这个恶意请求时,会自动附上用户的 Cookie。
  5. 目标网站执行操作:目标网站服务器收到请求和正确的 Cookie,认为这是用户的合法操作,于是执行了该请求(例如转账给攻击者)。

攻击示例

假设你登录了网上银行 bank.com,并且没有退出。然后你不小心点进了一个恶意网站。

这个恶意网站的页面上隐藏着这样一段 HTML 代码:

<img src="http://bank.com/transfer?amount=1000&to=attacker_account" style="display:none;">

你的浏览器加载这个图片时,会向 bank.com 发送一个 GET 请求:http://bank.com/transfer?amount=1000&to=attacker_account

因为你还登录着银行,浏览器会自动带上你的银行 Session Cookie。银行服务器收到这个请求和正确的 Cookie,就会认为是你本人想转账 1000 元给 attacker_account,于是操作成功执行。

更隐蔽的攻击会使用自动提交的 POST 表单,而不仅仅是 GET 请求。


三、CSRF 和 XSS 的区别是什么?

这是一个非常常见的困惑点。它们的核心区别在于:

特性 CSRF (跨站请求伪造) XSS (跨站脚本攻击)
出发点 利用网站的信任(利用用户的登录状态) 破坏网站的信任(向网站注入恶意脚本)
攻击目标 主要攻击一个已登录的用户,以其身份执行操作。 主要攻击网站本身,窃取用户数据或进行其他恶意行为。
操作方式 欺骗用户的浏览器向目标网站发送请求 向网站注入恶意脚本,脚本在用户浏览器执行。
所需条件 用户必须已登录目标网站。 网站存在漏洞,能够注入并执行恶意代码。

简单说:XSS 是让恶意脚本在你的电脑上运行;CSRF 是借你的手,用你的身份去发送一个你不知道的请求。


四、如何防御 CSRF?

防御 CSRF 的核心思想是增加请求的不可伪造性,让攻击者无法构造出能被服务器认可的合法请求。主要方法有:

  1. 使用 CSRF Token(最常用、最有效的方法)

    • 原理:服务器生成一个随机、复杂的令牌(Token),将其嵌入到表单(或页面的 Meta 标签)中,同时保存在服务器Session里。当用户提交表单时,必须带上这个 Token。服务器会验证这个 Token 是否与 Session 中的一致。攻击者无法猜到或窃取到这个 Token,因此他们构造的请求会因缺少或 Token 错误而被拒绝。
    • 关键:Token 必须随机、保密且与用户会话关联。
  2. 验证 Referer/Origin Header

    • 原理:服务器检查请求头中的 RefererOrigin 字段,判断请求是否来自合法的源(即自己的网站域名)。如果来自未知的第三方网站,则拒绝请求。
    • 缺点:有些用户的浏览器可能出于隐私原因禁用了 Referer,或者某些代理会剥离这个字段,导致合法请求被误杀。此方法通常作为辅助手段,但不能作为唯一的防御手段。
  3. 设置 SameSite Cookie 属性(现代浏览器支持)

    • 原理:这是一种更底层的防御。在设置 Cookie 时,可以指定 SameSite 属性。
      • SameSite=Strict:最严格,完全禁止第三方 Cookie。只有在当前网站的域名下请求时,才会发送 Cookie。
      • SameSite=Lax:宽松一些,允许在安全的三方请求(如 GET 导航)中发送 Cookie,但 POST 请求等仍会被禁止。这是目前很多浏览器的默认行为。
    • 效果:设置了 SameSite=Lax/Strict 后,从恶意网站发起的请求将不会自动携带目标网站的认证 Cookie,从而从根本上消除了 CSRF 攻击的条件。

关于 "CSRF 攻击" 的总结

项目 描述
中文名 跨站请求伪造
本质 利用用户的登录状态,冒用其身份进行非法操作
关键条件 用户已登录目标网站;用户访问了恶意链接或页面
防御手段 CSRF Token(主流)、验证 Referer/Origin(辅助)、SameSite Cookie(现代有效方式)

好的,为你的 PHP 网站防御 CSRF 攻击,最有效和常用的方法是使用 CSRF Token。下面我将为你提供一个清晰、完整且可操作的实现方案。

核心防御策略:CSRF Token

其工作流程可以概括为:

  1. 生成:在服务器端为每个用户会话(Session)生成一个唯一、随机、不可预测的 Token。
  2. 埋入:将这个 Token 传递给前端,嵌入到需要保护的每一个表单(或 AJAX 请求)中。
  3. 提交:用户提交表单时,这个 Token 会随着表单数据一同提交回服务器。
  4. 验证:服务器接收到请求后,比对提交来的 Token 和存储在用户 Session 中的 Token 是否一致。
  5. 决策:一致则请求合法,执行操作;不一致或缺失则拒绝请求,返回错误。

攻击者无法第三方网站无法窃取或猜到这个 Token,因此他们伪造的请求无法通过验证。


哪些场景适合使用 CSRF Token ?

理解哪些场景必须防御 CSRF 是实施安全措施的前提。简单来说,核心原则是:

任何会改变服务器状态或数据的“ authenticated request ”(已认证的请求)都必须防御 CSRF。

这里的“改变”包括创建、更新、删除操作(对应 RESTful 风格的 POST, PUT, PATCH, DELETE 请求)。如果这个操作需要用户登录才能执行,那么它就是 CSRF 攻击的目标。


必须防御 CSRF 的典型场景清单

你可以根据这个清单来检查你的应用,确保以下所有功能都受到保护:

1. 用户账户管理

这是最直接的目标,直接关系到账户安全。

  • 修改密码:攻击者可以永久将你锁在自己的账户外。
  • 更改邮箱地址:攻击者可以将其改为自己的邮箱,然后通过“忘记密码”功能接管账户。
  • 修改个人资料(如收货地址):攻击者可以将收货地址改为自己的,然后下单。
  • 绑定/解绑第三方服务(如微信、Google 登录)。

2. 金融与交易操作

这是最危险、最直接造成经济损失的场景。

  • 转账/支付:攻击者诱导你执行一笔向攻击者账户的转账。
  • 更改支付密码/方式
  • 申请贷款、理财

3. 内容创建与修改(Web 2.0 核心功能)

这是非常常见且容易被忽略的场景。

  • 发布文章/帖子/状态:攻击者可以用你的身份发布垃圾广告、恶意链接或不当言论,损害你的声誉。
  • 发表评论/留言:在他人内容下发布垃圾评论。
  • 点赞/收藏/关注:用于刷票、刷人气、制造虚假关注。
  • 删除内容:删除你重要的帖子或记录。

4. 后台管理系统 (Admin Panel)

后台是最高权限所在,一旦被攻破,后果是灾难性的。

  • 创建、修改、删除用户
  • 修改网站设置/配置
  • 审核内容(通过/拒绝)
  • 安装/卸载插件、主题(对于 CMS 如 WordPress)。

5. 用户提交数据

任何形式的提交操作。

  • 联系表单提交:用于发送垃圾邮件。
  • 工单提交:向客服系统发送大量垃圾信息。
  • 问卷调查提交:伪造调查数据。

6. API 请求

非常重要且易错的一点:如果你的 Web 应用提供 API(尤其是基于 Cookie/Session 认证的 API,而不是 Token-based),那么它的状态更改端点同样需要 CSRF 保护。

  • 一个前端 SPA(如 Vue, React)调用自家后端 API 进行修改操作,如果使用 Cookie 认证,就存在 CSRF 风险。

哪些场景通常不需要防御 CSRF?

同样重要的是,理解哪些场景不需要,可以避免过度工程。

  1. 纯粹的数据读取操作(GET 请求)

    • 浏览商品列表、查看文章详情、搜索内容。这些操作不改变服务器状态。
    • 注意:严格遵循 RESTful 规范时,GET 请求只用于读取。但如果某个 GET 请求会改变状态(这是一个糟糕的设计),那么它必须被保护。
  2. 公开的、无需登录的操作

    • 用户未登录时提交的公开表单(如注册表单、登录表单、公开的留言板)。因为攻击者无法利用用户的“已认证状态”。
    • 关键区别:登录请求本身有时需要保护,但不是因为 CSRF,而是为了防止登录 CSRF(攻击者用自己的凭证让你在不知情下登录他的账户,从而窥探你的隐私数据)。
  3. 使用非浏览器客户端发起的请求

    • 原生手机 App、桌面应用、服务器对服务器的通信。这些场景不使用浏览器的 Cookie 机制,因此不受 CSRF 影响。

总结与核心原则

场景特征 是否需要防御 CSRF? 例子
需要登录 + 修改数据 必须防御 改密码、转账、发帖、删帖
需要登录 + 只读数据 通常不需要 查看个人邮件列表、加载购物车
无需登录 + 任何操作 不需要 注册、登录、查看公开页面
非浏览器环境 不需要 App 调用 API、服务器间通信

最终建议:养成安全习惯 对于新手而言,最安全的做法是:对所有通过 Cookie/Session 认证的 POST、PUT、PATCH、DELETE 请求都默认启用 CSRF 防护。这是一个不会出错的安全实践。只有当你能明确判断某个端点绝对没有风险时,才将其排除。

CSRF Token 的实现和校验

具体实现步骤 PHP 代码示例

我们将创建两个核心函数和一个表单示例。

步骤 1:生成并存储 Token

首先,创建一个函数来生成 Token。这个 Token 必须是 cryptographically secure(加密学上安全)的随机值。

// file: csrf_functions.php
<?php
session_start(); // 确保每个会话都已启动
function generate_csrf_token() {
    // 检查是否已经为当前会话生成了 token,避免重复生成
    if (empty($_SESSION['csrf_token'])) {
        // 生成一个随机字节串(推荐方法,PHP 7+)
        // bin2hex 将其转换为十六进制字符串,便于在 HTML 中传输
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // 32字节 = 256位,非常安全
        // 对于旧版 PHP (< 7),可以用此方法(不推荐,仅作备选)
        // $_SESSION['csrf_token'] = md5(uniqid(mt_rand(), true));
    }
    return $_SESSION['csrf_token'];
}
?>

步骤 2:在表单中嵌入 Token

然后,在每个需要保护的 PHP 表单模板中,添加一个隐藏字段来输出这个 Token。

// file: form_page.php
<?php
require_once 'csrf_functions.php'; // 引入刚才的函数文件
$csrf_token = generate_csrf_token(); // 生成或获取 token
?>
<form action="process_form.php" method="POST">
    <!--- 你的其他表单字段,例如: -->
    <label for="amount">转账金额:</label>
    <input type="text" id="amount" name="amount">
    <!--- 关键的 CSRF Token 隐藏字段 -->
    <input type="hidden" name="csrf_token" value="<?php echo $csrf_token; ?>">
    <input type="submit" value="提交">
</form>

步骤 3:验证提交的 Token

最后,在处理表单提交的脚本中,编写验证逻辑。

// file: process_form.php
<?php
session_start(); // 必须启动 session 才能读取之前存储的 token
function validate_csrf_token($submitted_token) {
    // 1. 检查 session 中的 token 是否存在
    if (empty($_SESSION['csrf_token'])) {
        return false; // 源头就没有 token,非常可疑!
    }
    // 2. 检查提交过来的 token 是否存在
    if (empty($submitted_token)) {
        return false; // 请求中没有携带 token,拒绝
    }
    // 3. 比较两个 token 是否完全相同
    // 使用 hash_equals 防止时序攻击(Timing Attack)
    return hash_equals($_SESSION['csrf_token'], $submitted_token);
}
// --- 处理请求的逻辑开始 ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 获取提交上来的 token
    $submitted_token = $_POST['csrf_token'] ?? '';
    // 进行验证
    if (validate_csrf_token($submitted_token)) {
        // 验证成功!是合法请求
        // 这里执行你的核心业务逻辑,比如处理转账、更新数据等
        echo "请求验证成功!正在处理...";
        // e.g., $amount = $_POST['amount']; ...
        // 【可选】使用一次后立即销毁 token,强制下一个表单使用新 token(更安全)
        // unset($_SESSION['csrf_token']);
    } else {
        // 验证失败!是恶意请求或无效请求
        // 记录日志、告知用户、终止程序
        http_response_code(403); // Forbidden
        die("CSRF token 验证失败!请求无效。");
    }
} else {
    // 如果不是 POST 请求,也直接拒绝
    die('无效的请求方法。');
}
?>

重要注意事项和最佳实践

  1. Session 依赖:整个机制严重依赖 PHP Session。请确保 session_start() 在脚本最开始时就被调用。
  2. 每个会话唯一:Token 是存储在 $_SESSION 中的,所以每个登录的用户都会有自己独立的 Token。
  3. Token 强度:务必使用 random_bytes()openssl_random_pseudo_bytes() 来生成强随机数,绝对不要rand()mt_rand()time() 等简单易猜的值。
  4. 使用 hash_equals() 进行比较:不要使用 ===== 来比较字符串。hash_equals() 可以防止时序攻击(Timing Attack),即攻击者通过比较服务器响应时间的细微差异来猜测 Token。
  5. 保护所有状态更改操作:不仅限于 POST 表单,所有会修改服务器数据或状态的 GET 请求(虽然不推荐用 GET 做修改操作)也应该受到保护。通常 Token 也可以通过 URL 参数传递,但安全性不如放在 POST Body 中。
  6. 一次性使用(可选):为了极致安全,可以让 Token 只使用一次后就立即失效(注销 $_SESSION['csrf_token'])。但这可能会影响用户点击“后退”按钮后重新提交表单的体验,需要根据业务场景权衡。对于大多数场景,一个会话周期内使用同一个 Token 是安全的。
  7. AJAX 请求:如果你的网站使用 AJAX:
    • 同样需要将 Token 添加到请求中。
    • 可以将 Token 放在一个 <meta> 标签里(<meta name="csrf-token" content="<?= $csrf_token ?>">),然后让 JavaScript 读取并将其作为请求头(如 X-CSRF-TOKEN)发送。
    • 服务器端则需要检查这个请求头。

CSRF Token 的实现和校验总结

PHP 网站实现 CSRF 防护的三步骤:

  1. 生成:用 random_bytes() 生成强随机 Token,存入 Session。
  2. 嵌入:在每一个表单里用一个隐藏字段 <input type="hidden" name="csrf_token"> 输出这个 Token。
  3. 验证:在处理脚本里,用 hash_equals() 比较提交的 Token 和 Session 里的 Token。

为 PHP 网站设置 SameSite

为 PHP 网站设置 Cookie 的 SameSite 属性是现代防御 CSRF 和某些会话固定攻击非常有效且推荐的方法。它直接在传输层阻止浏览器在跨站请求中发送 Cookie,从根本上解决问题。

在 PHP 中,你有两种主要方式来设置 SameSite 属性:

  1. 使用 session_set_cookie_params() 函数(用于会话 Cookie,即 PHPSESSID)
  2. 使用 setcookie() 函数(用于你自定义设置的任何其他 Cookie)

方法一:为会话 Cookie (PHPSESSID) 设置 SameSite

会话 Cookie(通常是 PHPSESSID)是 CSRF 攻击的主要利用目标。保护它是重中之重。

最佳实践:在你的 PHP 脚本的最顶部、任何输出之前设置。

示例代码:设置会话 Cookie 的 SameSite 属性

<?php
// 必须在 session_start() 之前调用!
// 设置会话 cookie 的参数
session_set_cookie_params([
    'lifetime' => 3600, // cookie 有效期,秒
    'path' => '/',      // cookie 的有效路径
    'domain' => '.yourdomain.com', // 可选:有效域名(前面的点号允许子域名访问)
    'secure' => true,   // 仅通过 HTTPS 发送
    'httponly' => true, // 仅可通过 HTTP 协议访问,JS 无法读取
    'samesite' => 'Lax' // 设置 SameSite 属性为 Lax
]);
// 现在启动会话
session_start();
// ... 你网站接下来的代码 ...
?>

参数解释:

  • 'secure' => true: 至关重要SameSite 属性在现代浏览器中要求,如果 Secure 标志为 falseSameSite=None 将被拒绝。即使你设置为 LaxStrict,也强烈建议启用 Secure(即全站 HTTPS)。
  • 'samesite' => 'Lax': 这是目前平衡安全性和用户体验的最佳选择
    • 'Lax': (推荐默认值) 允许在安全的跨站 GET 请求(如用户从另一个网站点击链接到你的站)中发送 Cookie。但会阻止危险的跨站 POST 请求(如表单提交),从而有效防御 CSRF。
    • 'Strict': (最严格) 完全禁止第三方 Cookie。即使用户从谷歌搜索结果点击进入你的网站,也不会发送会话 Cookie,需要重新登录。安全性最高,但可能影响用户体验。
    • 'None': Cookie 将在所有上下文中发送,即跨站请求也会发送。你必须同时设置 'secure' => true。这通常用于需要嵌入在 iframe 中或第三方身份验证等场景,但会完全禁用 CSRF 保护,需额外措施。

方法二:为自定义 Cookie 设置 SameSite

对于使用 setcookie()setrawcookie() 函数设置的自定义 Cookie,你需要手动构建 Set-Cookie 头字符串,因为旧版 PHP 的 setcookie 函数不支持 samesite 参数。

从 PHP 7.3.0 开始setcookie() 函数增加了一个数组形式的选项参数,可以直接支持 samesite

示例代码 (PHP 7.3+)

<?php
// 设置一个自定义 Cookie 并指定 SameSite (PHP 7.3+ 语法)
setcookie('my_cookie', 'cookie_value', [
    'expires' => time() + 3600,
    'path' => '/',
    'domain' => 'yourdomain.com',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax' // 直接在这里设置
]);
?>

示例代码 (PHP < 7.3 的兼容方法)

如果你的服务器是旧版 PHP,你需要使用 header() 函数来手动设置。

<?php
// 设置一个自定义 Cookie 并指定 SameSite (PHP < 7.3 的兼容方法)
$cookie_name = 'my_cookie';
$cookie_value = 'cookie_value';
$expires = time() + 3600;
$path = '/';
$domain = 'yourdomain.com';
$secure = true; // 仅 HTTPS
$httponly = true;
$samesite = 'Lax';
// 构建 Set-Cookie 头部字符串
header(
    sprintf(
        'Set-Cookie: %s=%s; expires=%s; path=%s; domain=%s; %s %s samesite=%s',
        $cookie_name,
        urlencode($cookie_value),
        gmdate('D, d M Y H:i:s T', $expires),
        $path,
        $domain,
        $secure ? 'secure;' : '',
        $httponly ? 'httponly;' : '',
        $samesite
    )
);
?>

总结与最佳实践建议

  1. 首要任务:保护会话 Cookie

    • 使用 session_set_cookie_params(['samesite' => 'Lax', 'secure' => true, ...]),并确保在 session_start() 之前调用。
    • 这将你的 PHPSESSID Cookie 保护起来,这是防御 CSRF 最关键的一步。
  2. 选择正确的策略

    • 绝大多数网站:使用 SameSite=Lax。它提供了强大的 CSRF 防护,且不会破坏正常的用户导航体验。
    • 需要最高安全级别的网站(如网银):可以考虑 SameSite=Strict,但要接受用户从外部链接点击进来时需要重新登录的可能。
    • 需要嵌入在第三方网站(如 iframe 中的小工具、第三方登录):必须使用 SameSite=None; Secure
  3. 强制使用 HTTPS

    • 设置 SameSite 属性(尤其是 None)时,Secure 标志是强制要求。即使不使用 None,启用 Secure 也是一个绝对的最佳实践。
  4. 组合防御

    • 虽然 SameSite=Lax 能防御大多数 CSRF 攻击(特别是 POST 请求),但它不是银弹
    • 最安全的做法是采用深度防御策略:同时使用 SameSite=Lax CSRF Token。这样即使未来浏览器行为发生变化或有某些边缘情况,你的 Token 仍然是坚固的后防线。
发表评论
博客分类