跳转到内容

如何实现 CSRF 保护

编辑此页

CSRF,或 跨站请求伪造,是一种恶意行为者诱骗用户在 Web 应用程序上执行他们不知情或不同意的操作的攻击类型。

这种攻击基于 Web 应用程序对用户浏览器(例如,会话 Cookie)的信任。这是一个 CSRF 攻击的真实示例:恶意行为者可以创建以下网站

1
2
3
4
5
6
7
8
9
10
11
12
<html>
    <body>
        <form action="https://example.com/settings/update-email" method="POST">
            <input type="hidden" name="email" value="malicious-actor-address@some-domain.com"/>
        </form>
        <script>
            document.forms[0].submit();
        </script>

        <!-- some content here to distract the user -->
    </body>
</html>

如果您访问此网站(例如,通过点击某些电子邮件链接或某些社交网络帖子),并且您已经登录到 https://example.com 站点,则恶意行为者可以更改与您的帐户关联的电子邮件地址(有效地接管您的帐户),而您甚至没有意识到这一点。

防止 CSRF 攻击的有效方法是使用反 CSRF 令牌。这些是添加到表单中作为隐藏字段的唯一令牌。合法的服务器验证它们以确保请求来自预期的来源,而不是来自其他恶意网站。

安装

Symfony 提供了生成和验证反 CSRF 令牌所需的所有功能。在使用它们之前,请在您的项目中安装此软件包

1
$ composer require symfony/security-csrf

然后,使用 csrf_protection 选项启用/禁用 CSRF 保护(有关更多信息,请参阅 CSRF 配置参考

1
2
3
4
# config/packages/framework.yaml
framework:
    # ...
    csrf_protection: ~

用于 CSRF 保护的令牌对于每个用户都是不同的,并且存储在会话中。这就是为什么当您呈现带有 CSRF 保护的表单时,会自动启动会话。

此外,这意味着您无法完全缓存包含 CSRF 保护表单的页面。作为替代方案,您可以

  • 将表单嵌入到未缓存的 ESI 片段 中,并缓存页面的其余内容;
  • 缓存整个页面,并通过未缓存的 AJAX 请求加载表单;
  • 缓存整个页面,并使用 hinclude.js 通过未缓存的 AJAX 请求加载 CSRF 令牌,并将表单字段值替换为它。

Symfony 表单中的 CSRF 保护

Symfony 表单 默认包含 CSRF 令牌,Symfony 也会自动为您检查它们。因此,当使用 Symfony 表单时,您无需执行任何操作即可受到 CSRF 攻击的保护。

默认情况下,Symfony 在名为 _token 的隐藏字段中添加 CSRF 令牌,但这可以(1)全局自定义所有表单和(2)在每个表单的基础上自定义。全局来说,您可以在 framework.form 选项下配置它

1
2
3
4
5
6
7
# config/packages/framework.yaml
framework:
    # ...
    form:
        csrf_protection:
            enabled: true
            field_name: 'custom_token_name'

在每个表单的基础上,您可以在每个表单的 setDefaults() 方法中配置 CSRF 保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/Form/TaskType.php
namespace App\Form;

// ...
use App\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;

class TaskType extends AbstractType
{
    // ...

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class'      => Task::class,
            // enable/disable CSRF protection for this form
            'csrf_protection' => true,
            // the name of the hidden HTML field that stores the token
            'csrf_field_name' => '_token',
            // an arbitrary string used to generate the value of the token
            // using a different string for each form improves its security
            'csrf_token_id'   => 'task_item',
        ]);
    }

    // ...
}

您还可以通过创建自定义 表单主题 并使用 csrf_token 作为字段的前缀来自定义 CSRF 表单字段的呈现(例如,定义 {% block csrf_token_widget %} ... {% endblock %} 以自定义整个表单字段内容)。

手动生成和检查 CSRF 令牌

虽然 Symfony 表单默认提供自动 CSRF 保护,但您可能需要手动生成和检查 CSRF 令牌,例如在使用非 Symfony 表单组件管理的常规 HTML 表单时。

考虑创建一个 HTML 表单来允许删除项目。首先,使用 csrf_token() Twig 函数 在模板中生成 CSRF 令牌,并将其存储为隐藏表单字段

1
2
3
4
5
6
<form action="{{ url('admin_post_delete', { id: post.id }) }}" method="post">
    {# the argument of csrf_token() is an arbitrary string used to generate the token #}
    <input type="hidden" name="token" value="{{ csrf_token('delete-item') }}">

    <button type="submit">Delete item</button>
</form>

然后,在控制器操作中获取 CSRF 令牌的值,并使用 isCsrfTokenValid() 方法检查其有效性

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// ...

public function delete(Request $request): Response
{
    $submittedToken = $request->getPayload()->get('token');

    // 'delete-item' is the same value used in the template to generate the token
    if ($this->isCsrfTokenValid('delete-item', $submittedToken)) {
        // ... do something, like deleting an object
    }
}

或者,您可以使用控制器操作上的 IsCsrfTokenValid 属性

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
// ...

#[IsCsrfTokenValid('delete-item', tokenKey: 'token')]
public function delete(): Response
{
    // ... do something, like deleting an object
}

假设您想要每个项目的 CSRF 令牌,因此在模板中您有类似以下内容

1
2
3
4
5
6
<form action="{{ url('admin_post_delete', { id: post.id }) }}" method="post">
    {# the argument of csrf_token() is a dynamic id string used to generate the token #}
    <input type="hidden" name="token" value="{{ csrf_token('delete-item-' ~ post.id) }}">

    <button type="submit">Delete item</button>
</form>

IsCsrfTokenValid 属性也接受评估为 ID 的 Expression 对象

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
// ...

#[IsCsrfTokenValid(new Expression('"delete-item-" ~ args["post"].getId()'), tokenKey: 'token')]
public function delete(Post $post): Response
{
    // ... do something, like deleting an object
}

7.1

IsCsrfTokenValid 属性是在 Symfony 7.1 中引入的。

CSRF 令牌和压缩侧信道攻击

BREACHCRIME 是针对使用 HTTP 压缩时 HTTPS 的安全漏洞。攻击者可以利用压缩泄漏的信息来恢复目标明文部分。为了缓解这些攻击并防止攻击者猜测 CSRF 令牌,随机掩码会添加到令牌前面并用于扰乱它。

这项工作,包括代码示例,已获得 Creative Commons BY-SA 3.0 许可。
目录
    版本