Django 随笔: CSRF 防御
什么是 CSRF?
维基百科定义:https://en.wikipedia.org/wiki/Cross-site_request_forgery
CSRF跨站点请求伪造(Cross—Site Request Forgery),为了便于理解,我们引用http://blog.csdn.net/stpeace/article/details/53512283上的例子进行说明。
例子中涉及以下三个角色:
Web A : 存在安全漏洞的网站;
Web B :进行 CSRF 攻击的恶意网站;
User C: Web A 的网站的授权用户。
CSRF 攻击过程:
-
User C 在浏览器中打开 Web A,并输入用户名和密码进行登录;
-
Web A 对 User C 的登录信息进行验证并通过后,Web A 生成 cookie 信息并返回给浏览器,User C 登录成功,可以向 Web A 发送请求;
-
User C 在没有退出 Web A 的情况下,在同一个浏览器中,打开了一个访问 Web B 的页面;
-
Web B 在 User C 不知情的情况下向 Web A 发送请求(在请求头存放浏览器中 User C 的 Cookie 信息);
-
Web A 根据请求中的 Cookie 信息认为该请求是 User C 发送的,因此以 User C 的权限处理 Web B 发送请求。
这样 Web B 可以通过 User C 的权限对 Web A 进行操作。
如果 web A 为银行网站,Web B 可能通过这些权限在 User C 不知情的情况下转走 User C 账户中的资金。
Django CSRF 解决方案
Django 1.11 版本官方文档对此有比较全面的介绍,文档信息为:
原文地址:https://docs.djangoproject.com/en/1.11/ref/csrf/
翻译地址:https://www.jianshu.com/p/2b6c69f2d520
Django 什么时候进行 CSRF 防御?
Django 为使用 django-admin startproject 命令创建的项目自动设置 csrfviewmiddleware 后端进行 CSRF 防御。
csrfviewmiddleware 会对 POST 、PUT 和 DELETE 等请求进行检查,如果这些请求不包含 csrf token 或者 包含的 csrf token 不正确,会返回 403 Forbidden 响应。
解决 403 Forbidden 的简单方法
这是我们开始 Django 开发时经常遇到的一个问题。解决 '403 Forbidden' 最简单的一个方法是取消 Django 的 csrf 防御设置,我们可以采用以下两种方法实现:
-
打开 settings.py 文件,注释掉或者删除 MIDDLEWARE 设置中的 'django.middleware.csrf.CsrfViewMiddleware'。
-
为处理 POST 请求的视图添加 csrf_exempt 装饰器。
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
@csrf_exempt
def my_view(request):
return HttpResponse('Hello world')</pre>
这两种方法可以解决 '403 Forbidden' 的问题,但是却禁用了 csrfviewmiddleware 后端,网站遇到 CSRF 攻击将无法进行防御。
因此,我们需要做的是想办法通过 csrfviewmiddleware 后端的检查。
正确的方法
csrfviewmiddleware 对 POST 、PUT 和 DELETE 等请求进行以下检查:
-
cookie 和 POST ( PUT 或 DELETE ) 数据中必须包含 csrf token 信息;
-
cookie 和 POST ( PUT 或 DELETE ) 数据中的 csrf token 必须一致。
如果我们能满足以上两个要求,即可通过 csrfviewmiddleware 的验证。
下面,我们解决 Django 开发中最常用的 表单 POST 请求和 AJAX POST 请求的设置方法。
Form 表单
只需要表单模板的 <form> 元素中加入 csrf_token 模板标签即可:
<form action="" method="post">
{% csrf_token %}
然后使用 render() 函数或者通用视图渲染表单模板即可。后台会自动把 {% csrf_token %}
渲染为
<input type='hidden' name='csrfmiddlewaretoken' value='****' />
这样 POST 的数据中将会通过名为 csrfmiddlewaretoken 的键值对包含 csrf token 的信息,与此同时,后台还会在 cookie 中添加 csrf_token。
这样,即可通过 csrfviewmiddleware 的检验需求。
AJAX
AJAX 可以采用以下三种方法来处理。
方法 1 :
一劳永逸的方法,使用 Django 提供的方法:为满足条件的 XMLHttpRequest 设置一个自定义 X-CSRFToken标头保存 CSRF token 。由于许多 JavaScript 框架提供为每个请求设置标头的 hooks,这样做会很简单。
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader('X-CSRFToken', '{{ csrf_token }}');
}
}
});
这里设置了添加 csrf_token 的前提条件,要求满足 !csrfSafeMethod(settings.type) && !this.crossDomain 才可以添加,这样做是为了避免 csrf_token 泄露。
注意,这里使用的是 {{ csrf_token }},Django 将其渲染为 token 的值,并在 cookie 中添加 csrf_token。这样也可以满足 POST 数据和 cookie 中包含相同的值。
方法2
在 ajaxSetup 的 data 中设置 csrf_token,这样会为 ajax post( put、delete )请求的 data 添加 csrf_token:
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
data:{csrfmiddlewaretoken:'{{ csrf_token }}'}
}
}
});
在一般情况下, 方法2 等效于方法1,但是如果 ajax post 传输序列化数据,比如:
$.post('/', $('form').serialize())
jQuery 不能将序列化数据与ajaxSetup data 中的数据进行合并,从而造成 ajax 的 POST 数据中不包含 csrf_token。
方法3:
为每个 POST 请求的 ajax 的 data 中添加 csrf_token ,也就是:
$.ajax({
url:"...",
data:{"csrfmiddlewaretoken":'{{ csrf_token }}','other_key':'value'}
type:"POST",
success:function (data) {
alert(data)
}
})
})
这种方法简单,但是如果存在多个 ajax POST 请求,则比较麻烦。
csrf_token 模板标签如何正常工作
前面,我们使用了{% csrf_token %} 和 {{ csrf_token }} 模板标签。要保证 csrf_token 正常工作,需要启动 csrfviewmiddleware 后端 或者设置 csrf_protect 装饰器。如果这两者都没运行,则要使用 requests_csrf_token 装饰器。
强烈推荐使用上面的模板标签获取 csrf_token。如果不采用这个方法,在 AJAX 中,还可以根据 CSRF_USE_SESSION 的值的不同采用下面的方法从 cookie 或者 session获取 csrf_token。
从 cookie 中获取 csrf_token
<script src=" http://cdn.jsdelivr.net/jquery.cookie/1.4.1/jquery.cookie.min.js "></script>
<script>
var csrftoken = $.cookie('csrftoken');
这些代码从一个公共CDN加载jQuery cookie插件,这样我们可以与cookie交互,然后使用插件读取csrf token 的值。然后我们可以用 csrftoken 代替 {{ csrf_token }}。
从 session 中获取 csrf_token
我们可以这样读取 csrf_token:
{% csrf_token %}
<script type="text/javascript">
// using jQuery
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
</script>
这种方法的前提是 HTML中必须包含 CSRF token。