django 安全之防御 csrf

1,462 阅读3分钟
原文链接: zhuanlan.zhihu.com

本文主要通过分析 django 源码 介绍 django 在 csrf (跨站请求伪造) 的防御措施, 默认大家对 csrf 有一定的了解

0x01

一般问到如何防御 csrf, 很多人都会给这么个答案,请求的时候带一个 token,到服务器验证 token 的有效性。原理是没错,但是如果让你设计这个防御系统,需要考虑哪些呢,我觉得有这么几点

  • token 如何存储
  • token 如何验证
  • token 何时刷新

存储和验证相关联,有这么一种做法,每次用户登录生成随机值,存在数据库或者缓存中,请求的时候带上 token,每次去查询 token 有效性,这种方法其实就跟 session 验证用户是否登录一样。但问题是如果用户很多,验证的效率怎么保证。所以这种方法可行,但是成本不划算。第二种就是现在大多数的做法,cookie 里面带一个 csrf_token, 每次请求的时候也带上 token ,然后只要比较两者是否相同就行了,这样服务器没存任何东西,保证了效率。当然如果你的站点有 xss 之类的漏洞,cookie 也会泄露,同样不能保证这种方案的有效性。

token 刷新其实可以只要登录的时候刷新下,因为除非泄露,一般情况下 token 被暴力破解的概率不大。

0x02

下面看 django 如何实现的

django 对于 csrf 的防御主要在 middleware 实现,默认是开始 csrf 防御的,当然也可以关闭,单独对一些 view 开启,不过不建议这么做

try:
    csrf_token = _sanitize_token(
    request.COOKIES[settings.CSRF_COOKIE_NAME])
    # Use same token next time
    request.META['CSRF_COOKIE'] = csrf_token
except KeyError:
      csrf_token = None

这部分取得 cookie 里面的 csrf_token, 如果不存在则生成一个新的 csrf_token

request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')

这个是取得 post 请求的 request_csrf_token

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

对于 put delete 或者 ajax 请求来说,需要把 token 放到 headers 里面, 名字为 X-CSRFToken

最后只要比较 csrf_token 和 request_csrf_token 就能有效的防御 csrf

这里面有个特殊的地方,对于 http 来说,有可能存在中间人攻击,所以没法信任 HTTP_REFERER 这个 header ,但如果是 https, 做了下 HTTP_REFERER 的检查,只要 referer 不在信任域,一律拒绝。

referer = force_text(
     request.META.get('HTTP_REFERER'),
     strings_only=True,
     errors='replace'
 )

 if referer is None:
     return self._reject(request, REASON_NO_REFERER)
 # Here we generate a list of all acceptable HTTP referers,

 # including the current host since that has been validated

 # upstream.

 good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)

 # Note that request.get_host() includes the port.

 good_hosts.append(request.get_host())

 good_referers = ['https://{0}/'.format(host) for host in good_hosts]

 if not any(same_origin(referer, host) for host in good_referers):

     reason = REASON_BAD_REFERER % referer

     return self._reject(request, reason)

在登录的时候需要加上 rotate_token, 来刷新 token

def rotate_token(request):
    """
    Changes the CSRF token in use for a request - should be done on login

    for security purposes.
    """
    request.META.update({
        "CSRF_COOKIE_USED": True,
        "CSRF_COOKIE": _get_new_csrf_key(),
    })

0x03

总结下,django 框架本身对于安全性这块做的挺好的,对于工程师来说是福也是祸,能够更专注逻辑实现,但是有时候也因为忽视导致安全事故的发生。所以安全研发意识其实很重要,形成一些好的习惯,防患于未然。

我认为对于研发工程师而言,需要明白主流漏洞的原理,知道如何防御。比如就 web 安全而言, xss, csrf 之类。并且能够及时获取到最新一些安全信息,关注自己用的语言,框架的安全补丁。

如果对 web 安全感兴趣,想入门的话,知乎上有一些安全界的前辈, 比如 cos @余弦, 或者这些问答

自学成才的黑客(安全研究员)是从哪学到那些知识的

零基础如何学习 Web 安全