跨站请求伪造保护 框架实践

 

跨站请求伪造保护 | Django 文档 | Django https://docs.djangoproject.com/zh-hans/4.0/ref/csrf/

CSRF Protection - Laravel - The PHP Framework For Web Artisans https://laravel.com/docs/8.x/csrf

跨站请求伪造保护

CSRF 中间件和模板标签提供了易于使用的保护,防止 跨站请求伪造 。 当一个恶意网站包含一个链接、一个表单按钮或一些 JavaScript,目的是在你的网站上执行一些操作,使用在浏览器中访问恶意网站的登录用户的凭证时,就会发生这种类型的攻击。 此外,还包括一种相关的攻击类型,“登录 CSRF”,即攻击网站欺骗用户的浏览器使用他人的凭证登录网站。

对 CSRF 攻击的第一道防线是确保 GET 请求(和其他“安全”方法,如 RFC 7231#section-4.2.1 所定义的)没有副作用。通过“不安全”方法的请求,如 POST、PUT 和 DELETE,则可以通过以下步骤来保护。

如何使用它

要在你的视图中利用 CSRF 保护,请遵循以下步骤:

  1. CSRF 中间件默认在 MIDDLEWARE 配置中被激活。如果你覆盖了这个配置,请记住 'django.middleware.csrf.CsrfViewMiddleware' 应该排在任何假设 CSRF 攻击已经被处理的视图中间件之前。

    如果你禁用了它,这并不推荐,你可以使用 csrf_protect() 对你想要保护的特定视图进行保护(见下文)。

  2. 在任何使用 POST 表单的模板中,如果表单是针对内部 URL 的,请在 <form> 元素中使用 csrf_token 标签,例如:

    <form method="post">{% csrf_token %}
    

    对于以外部 URL 为目标的 POST 表单,不应该这样做,因为这会导致 CSRF 令牌泄露,从而导致漏洞。

  3. 在相应的视图函数中,确保 RequestContext 用于渲染响应,这样 {% csrf_token %} 才能正常工作。如果你使用的是 render()函数、通用视图或 contrib 应用程序,你已经被覆盖了,因为这些都使用 RequestContext

AJAX

虽然上述方法可以用于 AJAX POST 请求,但它有一些不便之处:你必须记住在每个 POST 请求中都要把 CSRF 令牌作为 POST 数据传递进来。出于这个原因,有一种替代方法:在每个 XMLHttpRequest 上,设置一个自定义的 X-CSRFToken 头(由 CSRF_HEADER_NAME 设置指定)为 CSRF 标记的值。这通常比较容易,因为许多 JavaScript 框架提供了钩子,允许在每个请求中设置头。

首先,你必须获得 CSRF 令牌。如何做取决于 CSRF_USE_SESSIONS 和 CSRF_COOKIE_HTTPONLY 配置是否启用。

在 AJAX 请求中设置令牌

最后,你需要在 AJAX 请求中设置头。使用 fetch() API:

const request = new Request(
    /* URL */,
    {
        method: 'POST',
        headers: {'X-CSRFToken': csrftoken},
        mode: 'same-origin' // Do not send CSRF token to another domain.
    }
);
fetch(request).then(function(response) {
    // ...
});

在 Jinja2 模板中使用 CSRF

Django 的 Jinja2 模板后端在所有模板的上下文中添加了 {{ csrf_input }},相当于 Django 模板语言中的 {% csrf_token %}。例如:

<form method="post">{{ csrf_input }}

装饰器方法

与其添加 CsrfViewMiddleware 作为全面保护,不如在需要保护的特定视图上使用 csrf_protect 装饰器,它具有完全相同的功能。它必须用于 同时 在输出中插入 CSRF 令牌的视图和接受 POST 表单数据的视图。(这些通常是相同的视图函数,但并不总是如此)。

不建议 单独使用装饰器,因为如果忘记使用,就会出现安全漏洞。“腰带和支架”的策略,两者同时使用也可以,而且会产生最小的开销。

csrf_protect(view)

为视图提供 CsrfViewMiddleware 保护的装饰器。

用法:

from django.shortcuts import render
from django.views.decorators.csrf import csrf_protect

@csrf_protect
def my_view(request):
    c = {}
    # ...
    return render(request, "a_template.html", c)

如果你使用的是基于类的视图,你可以参考 装饰基于类的视图

被拒绝的请求

默认情况下,如果传入的请求未能通过 CsrfViewMiddleware 执行的检查,则会向用户发送“403 Forbidden”响应。 这通常只应该出现在真正的跨站点请求伪造时,或者由于编程错误,CSRF 令牌没有被包含在 POST 表单中。

然而,错误页面不是很友好,所以你可能想提供自己的视图来处理这种情况。 要做到这一点,请设置 CSRF_FAILURE_VIEW 配置。

CSRF 失败会被记录为警告到 django.security.csrf 记录器。

工作方式

CSRF 保护是基于以下几点:

  1. 一个基于随机密钥值的 CSRF cookie,其他网站无法访问。

    这个 cookie 是由 CsrfViewMiddleware 设置的。如果在请求中还没有设置的话,那么它将与调用了 django.middleware.csrf.get_token() (内部用于获取 CSRF 令牌的函数)的每个响应一起发送。

    为了防止 BREACH 攻击,令牌不是简单的密钥,而是在密钥前面加上一个随机掩码,用来扰乱密钥。

    出于安全考虑,每次用户登录时都会改变密钥的值。

  2. 一个隐藏的表单字段,名称为“csrfmiddlewaretoken”,存在于所有发出的 POST 表单中。这个字段的值也是密钥的值,但有一个掩码,这个掩码会被添加到字段中,并被用来扰乱字段。掩码在每次调用 get_token() 时都会重新生成,所以表单字段的值在每次响应时都会改变。

    这一部分是由模板标签来完成的。

  3. 对于所有不使用 HTTP GET、HEAD、OPTIONS 或 TRACE 的传入请求,必须存在一个 CSRF cookie,并且“csrfmiddlewaretoken”字段必须存在且正确。如果不存在,用户将得到一个 403 错误。

    当验证“csrfmiddlewaretoken”字段值时,只有密钥,而不是完整的令牌,会与 cookie 值中的密钥进行比较。这允许使用不断变化的令牌。虽然每个请求都可能使用自己的令牌,但密钥对所有请求都是通用的。

    这个检查是由 CsrfViewMiddleware 完成的。

  4. CsrfViewMiddleware 根据当前主机和 CSRF_TRUSTED_ORIGINS 的设置,验证 Origin header ,如果是由浏览器提供的。这提供了对跨子域攻击的保护。

  5. 此外,对于 HTTPS 请求,如果没有提供 Origin 头,CsrfViewMiddleware 会执行严格的来源检查。这意味着,即使一个子域可以设置或修改你的域名上的 cookie,它也不能强迫用户向你的应用程序发布,因为该请求不会来自你自己的确切域名。

    这也解决了在 HTTPS 下使用独立于会话的密钥时可能出现的中间人攻击问题,这是因为 HTTP Set-Cookie 头会被客户接受(不幸的是),即使他们在 HTTPS 下与一个网站对话。对 HTTP 请求不进行 Referer 检查,因为 HTTP 下 Referer 头的存在不够可靠)。

    如果设置了 CSRF_COOKIE_DOMAIN 设置,则会将 referer 与之进行比较。你可以通过包含一个前导点号来允许跨子域请求。例如,CSRF_COOKIE_DOMAIN '.example.com' 将允许来自 www.example.com 和 api.example.com 的 POST 请求。如果没有设置,那么 referer 必须与 HTTP Host 头匹配。

    通过 CSRF_TRUSTED_ORIGINS 设置,可以将接受的 referer 扩展到当前主机或 cookie 域之外。

New in Django 4.0:

如上所述,增加了 Origin 检查。

这确保了只有源自受信任域的表单才能用于 POST 回数据。

它故意忽略了 GET 请求(以及被 RFC 7231#section-4.2.1 定义为“安全”的其他请求)。RFC 7231#section-4.2.1 将 POST、PUT 和 DELETE 定义为“不安全”,所有其他方法也被认为是不安全的,以获得最大的保护。

CSRF 保护不能防止中间人攻击,所以使用 HTTPS 与 HTTP 严格传输安全。它还假设 验证 HOST 头 和你的网站上没有任何 跨站脚本漏洞(因为 XSS 漏洞已经让攻击者做了 CSRF 漏洞允许的任何事情,甚至更糟)。

删除 Referer 头

为了避免向第三方网站透露 referrer URL,你可能想在你的网站的 <a> 标签上 禁用 referrer 。例如,你可以使用 <metaname="referrer" content="no-referrer"> 标签或包含 Referrer-Policy: no-referrer 头。由于 CSRF 保护对 HTTPS 请求进行严格的 referer 检查,这些技术会在使用“不安全”方法的请求上导致 CSRF 失败。取而代之的是,使用诸如 <arel="noreferrer" ...>" 这样的替代品来链接第三方网站。

缓存

如果 csrf_token 模板标签被模板使用(或 get_token 函数被其他方式调用),CsrfViewMiddleware 将添加一个 cookie 和一个 Vary: Cookie 头到响应中。这意味着,如果按照指示使用,中间件将与缓存中间件很好地配合(UpdateCacheMiddleware 先于所有其他中间件)。

但是,如果你在单个视图上使用缓存装饰器,CSRF 中间件还不能设置 Vary 头或 CSRF cookie,响应将在没有任何一个的情况下被缓存。在这种情况下,在任何需要插入 CSRF 令牌的视图上,你应该先使用 django.views.decorators.csrf.csrf_protect() 装饰器:

from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_protect

@cache_page(60 * 15)
@csrf_protect
def my_view(request):
    ...

如果你使用的是基于类的视图,你可以参考 装饰基于类的视图

测试中

CsrfViewMiddleware 通常会成为测试视图功能的一大障碍,因为需要 CSRF 令牌,而 CSRF 令牌必须在每个 POST 请求中发送。 出于这个原因,Django 的 HTTP 测试客户端已经被修改为在请求中设置一个标志,放松中间件和 csrf_protect 装饰器,使它们不再拒绝请求。 在其他方面(如发送 cookie 等),它们的行为是一样的。

如果出于某种原因,你  让测试客户端执行 CSRF 检查,你可以创建一个执行 CSRF 检查的测试客户端实例:

>>> from django.test import Client
>>> csrf_client = Client(enforce_csrf_checks=True)

限制

一个网站内的子域将能够在客户端设置整个域的 cookie。 通过设置 cookie 并使用相应的令牌,子域将能够规避 CSRF 保护。 避免这种情况的唯一方法是确保子域由受信任的用户控制(或者,至少无法设置 cookie)。 需要注意的是,即使没有 CSRF,也存在其他漏洞,比如会话固定,将子域交给不受信任的一方是个坏主意,而这些漏洞在目前的浏览器上是不容易修复的。

边缘案例

某些视图可能有不寻常的要求,这意味着它们不符合这里所设想的正常模式。在这些情况下,一些实用程序可能很有用。下一节将介绍可能需要它们的情况。

实用程序

下面的例子假设你使用的是基于函数的视图。如果你正在使用基于类的视图,你可以参考 装饰基于类的视图

csrf_exempt(view)

该装饰器标记着一个视图被免除了中间件所确保的保护。例如:

from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def my_view(request):
    return HttpResponse('Hello world')
requires_csrf_token(view)

通常情况下,如果 CsrfViewMiddleware.process_view 或类似 csrf_protect 这样的等价物没有运行, csrf_token 模板标签将无法工作。视图装饰器 requires_csrf_token 可以用来确保模板标签工作。这个装饰器的工作原理与 csrf_protect 类似,但绝不会拒绝接收到的请求。

举例:

from django.shortcuts import render
from django.views.decorators.csrf import requires_csrf_token

@requires_csrf_token
def my_view(request):
    c = {}
    # ...
    return render(request, "a_template.html", c)

该装饰器强制视图发送 CSRF cookie。

情境

仅仅是几个视图就应该禁用 CSRF 保护。

大多数视图需要 CSRF 保护,但也有少数视图不需要。

解决办法:与其禁用中间件并对所有需要的视图应用 csrf_protect,不如启用中间件并使用 csrf_exempt()

CsrfViewMiddleware.process_view 没有使用

有些情况下,CsrfViewMiddleware.process_view 可能在你的视图运行之前没有运行——例如 404 和 500 处理程序——但你仍然需要表单中的 CSRF 令牌。

解决方法:使用 requests_csrf_token()

不受保护的视图需要 CSRF 令牌

可能有一些视图是不受保护的,已经被 csrf_exempt 豁免,但仍然需要包括 CSRF 令牌。

解决方法:使用 csrf_exempt() 后面跟着 requires_csrf_token()。(即 requires_csrf_token 应该是最里面的装饰器)。

视图需要保护一条路径

一个视图只在一组条件下需要 CSRF 保护,其余时间一定不能有。

解决方法:用 csrf_exempt() 表示整个视图函数,用 csrf_protect() 表示其中需要保护的路径。例如:

from django.views.decorators.csrf import csrf_exempt, csrf_protect

@csrf_exempt
def my_view(request):

    @csrf_protect
    def protected_path(request):
        do_something()

    if some_condition():
       return protected_path(request)
    else:
       do_something_else()

页面使用 AJAX,没有任何 HTML 表单

一个页面通过 AJAX 进行 POST 请求,而该页面并没有一个带有 csrf_token 的 HTML 表单,这将导致所需的 CSRF cookie 被发送。

解决方法:在发送页面的视图上使用 sure_csrf_cookie()

Contrib 和可重用的应用

因为开发者可以关闭 CsrfViewMiddleware,所以 contrib 应用程序中的所有相关视图都使用 csrf_protect 装饰器来确保这些应用程序对 CSRF 的安全性。 建议其他可重用应用的开发者,如果想要得到同样的保证,也在他们的视图上使用 csrf_protect 装饰器。

常问问题

可以提交任意的 CSRF 令牌对(cookie 和 POST 数据)是漏洞吗?

不,这是设计好的。如果没有中间人攻击,攻击者就没有办法向受害者的浏览器发送 CSRF 令牌 cookie,所以成功的攻击需要通过 XSS 或类似的方式获得受害者浏览器的 cookie,在这种情况下,攻击者通常不需要 CSRF 攻击。

一些安全审计工具将此标记为问题,但如前所述,攻击者无法窃取用户浏览器的 CSRF cookie。使用 Firebug、Chrome 开发工具等“窃取”或修改 自己的 令牌并不是漏洞。

Django 的 CSRF 保护默认不与会话关联,是不是有问题?

不,这是设计好的。不将 CSRF 保护与会话联系起来,就可以在诸如 pastebin 这样允许匿名用户提交的网站上使用保护,而这些用户并没有会话。

如果你希望在用户的会话中存储 CSRF 令牌,请使用 CSRF_USE_SESSIONS 设置。

为什么用户登录后会遇到 CSRF 验证失败?

出于安全考虑,每次用户登录时,CSRF 令牌都会轮换。任何在登录前生成表单的页面都会有一个旧的、无效的 CSRF 令牌,需要重新加载。如果用户在登录后使用后退按钮或在不同的浏览器标签页中登录,可能会发生这种情况。

 

CSRF Protection

Introduction

Cross-site request forgeries are a type of malicious exploit whereby unauthorized commands are performed on behalf of an authenticated user. Thankfully, Laravel makes it easy to protect your application from cross-site request forgery (CSRF) attacks.

An Explanation Of The Vulnerability

In case you're not familiar with cross-site request forgeries, let's discuss an example of how this vulnerability can be exploited. Imagine your application has a /user/email route that accepts a POST request to change the authenticated user's email address. Most likely, this route expects an email input field to contain the email address the user would like to begin using.

Without CSRF protection, a malicious website could create an HTML form that points to your application's /user/email route and submits the malicious user's own email address:

<form action="https://your-application.com/user/email" method="POST">
<input type="email" value="malicious-email@example.com">
</form>
 
<script>
document.forms[0].submit();
</script>

If the malicious website automatically submits the form when the page is loaded, the malicious user only needs to lure an unsuspecting user of your application to visit their website and their email address will be changed in your application.

To prevent this vulnerability, we need to inspect every incoming POSTPUTPATCH, or DELETE request for a secret session value that the malicious application is unable to access.

Preventing CSRF Requests

Laravel automatically generates a CSRF "token" for each active user sessionmanaged by the application. This token is used to verify that the authenticated user is the person actually making the requests to the application. Since this token is stored in the user's session and changes each time the session is regenerated, a malicious application is unable to access it.

The current session's CSRF token can be accessed via the request's session or via the csrf_token helper function:

use Illuminate\Http\Request;
 
Route::get('/token', function (Request $request) {
$token = $request->session()->token();
 
$token = csrf_token();
 
// ...
});

Anytime you define a "POST", "PUT", "PATCH", or "DELETE" HTML form in your application, you should include a hidden CSRF _token field in the form so that the CSRF protection middleware can validate the request. For convenience, you may use the @csrf Blade directive to generate the hidden token input field:

<form method="POST" action="/profile">
@csrf
 
<!-- Equivalent to... -->
<input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>

The App\Http\Middleware\VerifyCsrfToken middleware, which is included in the webmiddleware group by default, will automatically verify that the token in the request input matches the token stored in the session. When these two tokens match, we know that the authenticated user is the one initiating the request.

CSRF Tokens & SPAs

If you are building an SPA that is utilizing Laravel as an API backend, you should consult the Laravel Sanctum documentation for information on authenticating with your API and protecting against CSRF vulnerabilities.

Excluding URIs From CSRF Protection

Sometimes you may wish to exclude a set of URIs from CSRF protection. For example, if you are using Stripe to process payments and are utilizing their webhook system, you will need to exclude your Stripe webhook handler route from CSRF protection since Stripe will not know what CSRF token to send to your routes.

Typically, you should place these kinds of routes outside of the web middleware group that the App\Providers\RouteServiceProvider applies to all routes in the routes/web.php file. However, you may also exclude the routes by adding their URIs to the $except property of the VerifyCsrfToken middleware:

<?php
 
namespace App\Http\Middleware;
 
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
 
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'stripe/*',
'http://example.com/foo/bar',
'http://example.com/foo/*',
];
}

{tip} For convenience, the CSRF middleware is automatically disabled for all routes when running tests.

X-CSRF-TOKEN

In addition to checking for the CSRF token as a POST parameter, the App\Http\Middleware\VerifyCsrfToken middleware will also check for the X-CSRF-TOKENrequest header. You could, for example, store the token in an HTML meta tag:

<meta name="csrf-token" content="{{ csrf_token() }}">

Then, you can instruct a library like jQuery to automatically add the token to all request headers. This provides simple, convenient CSRF protection for your AJAX based applications using legacy JavaScript technology:

$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});

X-XSRF-TOKEN

Laravel stores the current CSRF token in an encrypted XSRF-TOKEN cookie that is included with each response generated by the framework. You can use the cookie value to set the X-XSRF-TOKEN request header.

This cookie is primarily sent as a developer convenience since some JavaScript frameworks and libraries, like Angular and Axios, automatically place its value in the X-XSRF-TOKEN header on same-origin requests.

{tip} By default, the resources/js/bootstrap.js file includes the Axios HTTP library which will automatically send the X-XSRF-TOKEN header for you.

 

posted @ 2022-02-22 16:56  papering  阅读(469)  评论(0编辑  收藏  举报