CSRF 攻击防范策略
- 2025-08-22 20:05:00
- 丁国栋
- 原创 11
CSRF 攻击防范策略
什么是 CSRF 攻击?
一、核心概念:什么是 CSRF?
CSRF(Cross-Site Request Forgery),中文翻译为“跨站请求伪造”。它是一种常见的网络攻击方式。
你可以用一个简单的比喻来理解它:
“冒名顶替” 或 “借刀杀人”。
攻击者欺骗你的浏览器,让你在不知情的情况下,使用你亲自操作过的一个已认证用户的身份,向一个你原本信任的网站(比如你的网上银行、社交媒体或邮箱)发送一个伪造的请求,执行了某些操作(比如转账、改密码、发状态)。
关键点在于:攻击者利用的是你已经在该网站登录后的身份认证凭证(通常是浏览器 Cookie)。因为浏览器会自动在每次请求中带上对应网站的 Cookie,所以目标网站会认为这个请求是你本人自愿发出的。
二、CSRF 攻击是如何发生的?
一个典型的 CSRF 攻击需要同时满足以下几个条件:
- 用户已登录信任网站:用户已经登录了目标网站(如
bank.com
),并在浏览器中保存了登录凭证(Session Cookie)。 - 用户访问恶意网站:用户随后在同一个浏览器中,访问了攻击者精心构造的恶意网站或链接。
- 恶意请求被触发:这个恶意网站包含了一个会自动向目标网站(
bank.com
)发送请求的代码(比如一个隐藏的表单或一个<img>
标签)。 - 请求携带用户凭证:由于用户已登录,浏览器在发送这个恶意请求时,会自动附上用户的 Cookie。
- 目标网站执行操作:目标网站服务器收到请求和正确的 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 的核心思想是增加请求的不可伪造性,让攻击者无法构造出能被服务器认可的合法请求。主要方法有:
-
使用 CSRF Token(最常用、最有效的方法)
- 原理:服务器生成一个随机、复杂的令牌(Token),将其嵌入到表单(或页面的 Meta 标签)中,同时保存在服务器Session里。当用户提交表单时,必须带上这个 Token。服务器会验证这个 Token 是否与 Session 中的一致。攻击者无法猜到或窃取到这个 Token,因此他们构造的请求会因缺少或 Token 错误而被拒绝。
- 关键:Token 必须随机、保密且与用户会话关联。
-
验证 Referer/Origin Header
- 原理:服务器检查请求头中的
Referer
或Origin
字段,判断请求是否来自合法的源(即自己的网站域名)。如果来自未知的第三方网站,则拒绝请求。 - 缺点:有些用户的浏览器可能出于隐私原因禁用了 Referer,或者某些代理会剥离这个字段,导致合法请求被误杀。此方法通常作为辅助手段,但不能作为唯一的防御手段。
- 原理:服务器检查请求头中的
-
设置 SameSite Cookie 属性(现代浏览器支持)
- 原理:这是一种更底层的防御。在设置 Cookie 时,可以指定
SameSite
属性。SameSite=Strict
:最严格,完全禁止第三方 Cookie。只有在当前网站的域名下请求时,才会发送 Cookie。SameSite=Lax
:宽松一些,允许在安全的三方请求(如 GET 导航)中发送 Cookie,但 POST 请求等仍会被禁止。这是目前很多浏览器的默认行为。
- 效果:设置了
SameSite=Lax/Strict
后,从恶意网站发起的请求将不会自动携带目标网站的认证 Cookie,从而从根本上消除了 CSRF 攻击的条件。
- 原理:这是一种更底层的防御。在设置 Cookie 时,可以指定
关于 "CSRF 攻击" 的总结
项目 | 描述 |
---|---|
中文名 | 跨站请求伪造 |
本质 | 利用用户的登录状态,冒用其身份进行非法操作 |
关键条件 | 用户已登录目标网站;用户访问了恶意链接或页面 |
防御手段 | CSRF Token(主流)、验证 Referer/Origin(辅助)、SameSite Cookie(现代有效方式) |
好的,为你的 PHP 网站防御 CSRF 攻击,最有效和常用的方法是使用 CSRF Token。下面我将为你提供一个清晰、完整且可操作的实现方案。
核心防御策略:CSRF Token
其工作流程可以概括为:
- 生成:在服务器端为每个用户会话(Session)生成一个唯一、随机、不可预测的 Token。
- 埋入:将这个 Token 传递给前端,嵌入到需要保护的每一个表单(或 AJAX 请求)中。
- 提交:用户提交表单时,这个 Token 会随着表单数据一同提交回服务器。
- 验证:服务器接收到请求后,比对提交来的 Token 和存储在用户 Session 中的 Token 是否一致。
- 决策:一致则请求合法,执行操作;不一致或缺失则拒绝请求,返回错误。
攻击者无法第三方网站无法窃取或猜到这个 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?
同样重要的是,理解哪些场景不需要,可以避免过度工程。
-
纯粹的数据读取操作(GET 请求):
- 浏览商品列表、查看文章详情、搜索内容。这些操作不改变服务器状态。
- 注意:严格遵循 RESTful 规范时,GET 请求只用于读取。但如果某个 GET 请求会改变状态(这是一个糟糕的设计),那么它必须被保护。
-
公开的、无需登录的操作:
- 用户未登录时提交的公开表单(如注册表单、登录表单、公开的留言板)。因为攻击者无法利用用户的“已认证状态”。
- 关键区别:登录请求本身有时需要保护,但不是因为 CSRF,而是为了防止登录 CSRF(攻击者用自己的凭证让你在不知情下登录他的账户,从而窥探你的隐私数据)。
-
使用非浏览器客户端发起的请求:
- 原生手机 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('无效的请求方法。');
}
?>
重要注意事项和最佳实践
- Session 依赖:整个机制严重依赖 PHP Session。请确保
session_start()
在脚本最开始时就被调用。 - 每个会话唯一:Token 是存储在
$_SESSION
中的,所以每个登录的用户都会有自己独立的 Token。 - Token 强度:务必使用
random_bytes()
或openssl_random_pseudo_bytes()
来生成强随机数,绝对不要用rand()
、mt_rand()
或time()
等简单易猜的值。 - 使用
hash_equals()
进行比较:不要使用==
或===
来比较字符串。hash_equals()
可以防止时序攻击(Timing Attack),即攻击者通过比较服务器响应时间的细微差异来猜测 Token。 - 保护所有状态更改操作:不仅限于 POST 表单,所有会修改服务器数据或状态的 GET 请求(虽然不推荐用 GET 做修改操作)也应该受到保护。通常 Token 也可以通过 URL 参数传递,但安全性不如放在 POST Body 中。
- 一次性使用(可选):为了极致安全,可以让 Token 只使用一次后就立即失效(注销
$_SESSION['csrf_token']
)。但这可能会影响用户点击“后退”按钮后重新提交表单的体验,需要根据业务场景权衡。对于大多数场景,一个会话周期内使用同一个 Token 是安全的。 - AJAX 请求:如果你的网站使用 AJAX:
- 同样需要将 Token 添加到请求中。
- 可以将 Token 放在一个
<meta>
标签里(<meta name="csrf-token" content="<?= $csrf_token ?>">
),然后让 JavaScript 读取并将其作为请求头(如X-CSRF-TOKEN
)发送。 - 服务器端则需要检查这个请求头。
CSRF Token 的实现和校验总结
PHP 网站实现 CSRF 防护的三步骤:
- 生成:用
random_bytes()
生成强随机 Token,存入 Session。 - 嵌入:在每一个表单里用一个隐藏字段
<input type="hidden" name="csrf_token">
输出这个 Token。 - 验证:在处理脚本里,用
hash_equals()
比较提交的 Token 和 Session 里的 Token。
为 PHP 网站设置 SameSite
为 PHP 网站设置 Cookie 的 SameSite
属性是现代防御 CSRF 和某些会话固定攻击非常有效且推荐的方法。它直接在传输层阻止浏览器在跨站请求中发送 Cookie,从根本上解决问题。
在 PHP 中,你有两种主要方式来设置 SameSite
属性:
- 使用
session_set_cookie_params()
函数(用于会话 Cookie,即 PHPSESSID) - 使用
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
标志为false
,SameSite=None
将被拒绝。即使你设置为Lax
或Strict
,也强烈建议启用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
)
);
?>
总结与最佳实践建议
-
首要任务:保护会话 Cookie
- 使用
session_set_cookie_params(['samesite' => 'Lax', 'secure' => true, ...])
,并确保在session_start()
之前调用。 - 这将你的
PHPSESSID
Cookie 保护起来,这是防御 CSRF 最关键的一步。
- 使用
-
选择正确的策略
- 绝大多数网站:使用
SameSite=Lax
。它提供了强大的 CSRF 防护,且不会破坏正常的用户导航体验。 - 需要最高安全级别的网站(如网银):可以考虑
SameSite=Strict
,但要接受用户从外部链接点击进来时需要重新登录的可能。 - 需要嵌入在第三方网站(如 iframe 中的小工具、第三方登录):必须使用
SameSite=None; Secure
。
- 绝大多数网站:使用
-
强制使用 HTTPS
- 设置
SameSite
属性(尤其是None
)时,Secure
标志是强制要求。即使不使用None
,启用Secure
也是一个绝对的最佳实践。
- 设置
-
组合防御
- 虽然
SameSite=Lax
能防御大多数 CSRF 攻击(特别是 POST 请求),但它不是银弹。 - 最安全的做法是采用深度防御策略:同时使用
SameSite=Lax
和 CSRF Token。这样即使未来浏览器行为发生变化或有某些边缘情况,你的 Token 仍然是坚固的后防线。
- 虽然