【Django 024】中间件Middleware(三):Django自带csrf中间件源码分析以及跳过csrf报错的四种方法

Django作为一个重量级框架,其已经事先为我们内置了很多安全模块,CSRF中间件就是其中之一。那么究竟什么是CSRF,其中间件工作原理是怎样的,我们又应该如何去使用CSRF呢。这一篇文章我们一起来深入学习一下。

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

什么是CSRF

CSRF,全程Cross Site Request Forgery,中文名“跨站请求伪造”。

前面学习三种会话技术的时候,我们了解了cookie和session(session也借助于cookie)。假设用户小付在某网站www.alotofmoney.com已经经过认证获得了cookie或者session存储在本地浏览器,此时坏人发给小付一个网址,里面有个链接,点击以后会访问www.alotofmoney.com的一个汇款API。因为已经经过了认证,所以受害者小付此时用刚才同样的浏览器访问这个API是会成功的,即使这并不是他的本意。

这种利用已经被服务器信任的浏览器,伪造用户请求的攻击方式,就叫CSRF。

CSRF的防范策略

目前有下面三种方式可以用来防范CSRF:

  • 利用token

既然存储在浏览器中的cookie那么轻易被利用,那么就不用浏览器中的cookie了,利用本地数据库或者文件中记录的token信息来访问网站。关于token的使用可以参考我的另一篇博客《【Django 014】Django2.2会话技术之Token》

  • 确认referer

在网页跳转的时候,request头中会带有一个referer,在处理敏感请求的时候,referer应该和目标url属于同一个域名。而CSRF中的referer却是坏人自己的网站,所以会失败。但是类似爬虫可以修改请求header一样,难保坏人不去手动修改这个字段。所以这个检测手段简单但并不一定可靠

  • 给网页添加特殊cookie

这是Django中使用的方法。可以在网页中添加一些代码,使得该网页在发送请求的时候会带上一些特殊cookie,这个cookie在服务端可以被验证。如果验证通过标明是受信任的网页,反之则访问被拒绝。

Django中的CSRF中间件

下面重点来看看Django中实现CSRF的中间件。

新建的项目就会自带下面的这个中间件

'django.middleware.csrf.CsrfViewMiddleware',

点进去看看源码。

类的结构

先看下类大概长啥样
1-csrf.png
和所有中间件一样,继承自MiddlewareMixin,是一个类装饰器。除了魔术方式以外一共还有7个方法

  • process_request
  • process_view
  • process_response

这是我们前面说的5个切点中的3个,前两个在请求阶段,最后是在返回阶段。

  • _accept
  • _reject
  • _get_token
  • _set_token

这4个方法用来被上面3个切点调用,完成特定功能。以单下划线开头,表示不建议被外部访问。

下面就从数据流的方向依次看看这3个切点完成的功能。

process_request

其代码如下

def process_request(self, request):
    csrf_token = self._get_token(request)
    if csrf_token is not None:
        # Use same token next time.
        request.META['CSRF_COOKIE'] = csrf_token

本质就是从请求中拿到一个csrf_token的值,如果能拿到,放到请求的元数据中供后面使用。

重点看下用来获取这个csrf_token的函数_get_token

def _get_token(self, request):
    if settings.CSRF_USE_SESSIONS:
        try:
            return request.session.get(CSRF_SESSION_KEY)
        except AttributeError:
            raise ImproperlyConfigured(
                'CSRF_USE_SESSIONS is enabled, but request.session is not '
                'set. SessionMiddleware must appear before CsrfViewMiddleware '
                'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
            )
    else:
        try:
            cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
        except KeyError:
            return None

        csrf_token = _sanitize_token(cookie_token)
        if csrf_token != cookie_token:
            # Cookie token needed to be replaced;
            # the cookie needs to be reset.
            request.csrf_cookie_needs_reset = True
        return csrf_token

这里分了两种情况,if结构下是利用session来获取,else结构下是利用cookie来获取。

这里的两个配置项settings.CSRF_USE_SESSIONSsettings.CSRF_COOKIE_NAME是在全局配置文件django/conf/global_settings.py里面定义的

CSRF_COOKIE_NAME = 'csrftoken'
CSRF_USE_SESSIONS = False

所以这个函数的作用就是从cookie中获取csrftoken这个key对应的值,但即使没获取成功也不会影响后续步骤

process_view

其代码如下

def process_view(self, request, callback, callback_args, callback_kwargs):
    if getattr(request, 'csrf_processing_done', False):
        return None

    if getattr(callback, 'csrf_exempt', False):
        return None

    if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
        if getattr(request, '_dont_enforce_csrf_checks', False):
            return self._accept(request)

        if request.is_secure():
            referer = request.META.get('HTTP_REFERER')
            if referer is None:
                return self._reject(request, REASON_NO_REFERER)

            referer = urlparse(referer)

            if '' in (referer.scheme, referer.netloc):
                return self._reject(request, REASON_MALFORMED_REFERER)

            # Ensure that our Referer is also secure.
            if referer.scheme != 'https':
                return self._reject(request, REASON_INSECURE_REFERER)

            good_referer = (
                settings.SESSION_COOKIE_DOMAIN
                if settings.CSRF_USE_SESSIONS
                else settings.CSRF_COOKIE_DOMAIN
            )
            if good_referer is not None:
                server_port = request.get_port()
                if server_port not in ('443', '80'):
                    good_referer = '%s:%s' % (good_referer, server_port)
            else:
                try:
                    # request.get_host() includes the port.
                    good_referer = request.get_host()
                except DisallowedHost:
                    pass

            good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
            if good_referer is not None:
                good_hosts.append(good_referer)

            if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
                reason = REASON_BAD_REFERER % referer.geturl()
                return self._reject(request, reason)

        csrf_token = request.META.get('CSRF_COOKIE')
        if csrf_token is None:
            return self._reject(request, REASON_NO_CSRF_COOKIE)

        # Check non-cookie token for match.
        request_csrf_token = ""
        if request.method == "POST":
            try:
                request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
            except IOError:
                pass

        if request_csrf_token == "":
            request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')

        request_csrf_token = _sanitize_token(request_csrf_token)
        if not _compare_salted_tokens(request_csrf_token, csrf_token):
            return self._reject(request, REASON_BAD_TOKEN)

    return self._accept(request)

这里主要是三个大的if结构,然后第三个if又包含很多小的if

第一个if结构如下

if getattr(request, 'csrf_processing_done', False):
    return None

如果进来的请求带有csrf_processing_done的属性,则成功通过。这算是一个比较宽泛的豁免csrf认证的方式,只需要将某些进来的请求手动添加该属性即可达到豁免目的。

第二个if结构如下

if getattr(callback, 'csrf_exempt', False):
    return None

这里是针对单个view函数的豁免,如果某个view函数带有csrf_exempt属性,则请求该view函数时得到豁免。

第三个if结构是指不在('GET', 'HEAD', 'OPTIONS', 'TRACE')这四种请求方式中的操作,目前主要就是针对POST方式。里面又包含了三大块。

第一块如下

if getattr(request, '_dont_enforce_csrf_checks', False):
    return self._accept(request)

如果进来的请求带有_dont_enforce_csrf_checks属性,可以得到豁免。

第二块是针对HTTPS请求的额外处理,利用查看referer的方式来进行验证,不满足特定referer的话就直接会被拒绝,这里就不展开了。

重点是说一下最后一块。

第三块如下

csrf_token = request.META.get('CSRF_COOKIE')
if csrf_token is None:
    return self._reject(request, REASON_NO_CSRF_COOKIE)

# Check non-cookie token for match.
request_csrf_token = ""
if request.method == "POST":
    try:
        request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
    except IOError:
        pass

if request_csrf_token == "":
    request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')

request_csrf_token = _sanitize_token(request_csrf_token)
if not _compare_salted_tokens(request_csrf_token, csrf_token):
    return self._reject(request, REASON_BAD_TOKEN)

首先,获取process_request中拿到的cookie赋值给csrf_token,假如前面拿到的是None,直接被拒掉。

然后如果是POST请求,还会查看传递上来的数据中的csrfmiddlewaretoken对应的值,赋值给request_csrf_token。如果没有这个值,就尝试从元数据中获取。

最后对csrf_tokenrequest_csrf_token做某种事先规定好的判定,不符合条件就会被拒绝。

所以这里一共交代了下面三件事:

  • request级别的豁免方式
  • view函数级别的豁免方式
  • 针对POST方式,不仅需要带csrftoken这个cookie,还有传上来csrfmiddlewaretoken这个值得内容,并且这两者还要满足既定的关系才可以

待会我们就针对豁免和成功通过csrf来分别进行验证。

process_response

其代码如下

def process_response(self, request, response):
    if not getattr(request, 'csrf_cookie_needs_reset', False):
        if getattr(response, 'csrf_cookie_set', False):
            return response

    if not request.META.get("CSRF_COOKIE_USED", False):
        return response

    self._set_token(request, response)
    response.csrf_cookie_set = True
    return response

csrf得cookie有一定的更新周期,这里不展开说。这里会对需要设置或者更新cookie的情况进行处理,然后返回。

实际操作

还是以上一节《【Django 023】中间件Middleware(二):结合session和cache实现反爬虫中间件图文详解》中的那个登录页面为例,因为涉及到POST请求,所以会走csrf验证。

不加任何处理的post请求

login.html的form表单中,去掉处理csrf的标签{% csrf_token %}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<form action="{% url 'app:login' %}" method="post">
{#    {% csrf_token %}#}
    <label for="name">Name: </label><input type="text" id="name" name="name" placeholder="Your name">
    <input type="submit">
</form>
</body>
</html>

之后输入用户名登录,出现下面的403返回页面
2-403.png

成功csrf认证的post请求

按照上面错误页面的提示,在发出post请求的form表单中恢复{% csrf_token %},之后的效果如下
3-token.png
可以看到加了这个标签之后,post请求不仅包含了csrftoken的cookie,也上传了csrfmiddlewaretoken的数据。根据上面的分析,这种请求是可以通过的。

view函数豁免

有的时候我们开发了一些公共的API,并不是那么敏感,就可以将该API进行豁免。

前面提到说如果这个view方法有csrf_exempt属性的话,可以直接豁免。Djang为我们贴心的准备了一个单独的装饰器@csrf_exempt用来添加这一属性。

一个测试用的API如下

def test_api(request):
   if request.method == 'POST':
       name = json.loads(request.body).get('name')
       response = JsonResponse(data={'name': name, 'message': 'Welcome'})
   else:
       response = HttpResponse('Error')
   return response

题外话,这里是为了用curl命令测试方便所以采用request.body而不用request.POST来获取POST数据。具体可参见《Django获取curl传递POST数据失败解决方法》

之后用curl命令测试一下,发现出现csrf的403报错

[fuhx@testmachine ~]$ curl -X POST -d '{"name":"xiaofu"}' http://127.0.0.1:8000/app/test_api/

<!DOCTYPE html>
<html lang="en">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="robots" content="NONE,NOARCHIVE">
  <title>403 Forbidden</title>
...
...

假如加上了装饰器

@csrf_exempt
def test_api(request):
   if request.method == 'POST':
       name = json.loads(request.body).get('name')
       response = JsonResponse(data={'name': name, 'message': 'Welcome'})
   else:
       response = HttpResponse('Error')
   return response

结果如下

[fuhx@testmachine ~]$ curl -X POST -d '{"name":"xiaofu"}' http://127.0.0.1:8000/app/test_api/
{"name": "xiaofu", "message": "Welcome"}

成功通过了csrf认证。

request批量豁免

想要对符合条件的所有path的请求批量豁免,按照源码中的设定,就可以在自定义的中间件做一个预处理,给request加上csrf_processing_done的属性即可。如下

if request.path.startswith('/app/test_api/'):
    request.csrf_processing_done = True

此时去掉上面的@csrf_exempt装饰器依然可以成功访问API

[fuhx@testmachine ~]$ curl -X POST -d '{"name":"james"}' http://127.0.0.1:8000/app/test_api/
{"name": "james", "message": "Welcome"}

总结

这一节我们通过学习系统自带的csrf中间件源码,巩固了Django中间件的设计思路。同时学习了如下四种跳过csrf报错的方法:

  • 注释掉csrf中间件,简单粗暴
  • 在发送POST请求的form表单中添加{% csrf_token %}标签,完整通过csrf验证
  • 对需要豁免的view函数添加@csrf_exempt装饰器
  • 对批量的request请求添加csrf_prcessing_done属性

出于安全考虑,除了公共API可以豁免外,还是建议在自己的应用中加入csrf认证。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值