zoukankan      html  css  js  c++  java
  • django中间件CsrfViewMiddleware源码分析,探究csrf实现

    Django Documentation

    csrf保护基于以下: 
    1. 一个CSRF cookie 基于一个随机生成的值,其他网站无法得到。此cookie由CsrfViewMiddleware产生。它与每个调用django.middleware.csrf.get_token()(这是一个用于取回CSRF token的方法)的响应一起发送,如果它尚未在请求上设置的话。
    为了防止BREACH攻击,token不仅仅是秘密;随机的salt被置于secret之前并用来加密它。出于安全原因,每次用户登录时都会更改密钥的值。

    1. 所有传出POST表单中都有一个名为csrfmiddlewaretoken的隐藏表单字段。此字段的值同样是秘密的值。salt添加到它并用于加扰它。每次调用get_token()时都会重新生成salt,以便在每个此类响应中更改表单字段值。这部分由template的{% csrf_token %}完成。

    2. 对于未使用HTTP GETHEADOPTIONSTRACE的所有传入请求,必须带有CSRF cookie,并且csrfmiddlewaretoken字段必须存在且正确。如果不是,用户将收到403错误。
      验证csrfmiddlewaretoken字段值时,只将secret而不是整个token与cookie值中的secret进行比较。这允许使用不断变化的token。虽然每个请求都可以使用自己的token,但secret仍然是所有人共同的。
      此检查由CsrfViewMiddleware完成。

    3. 此外,对于HTTPS请求,严格的引用检查由CsrfViewMiddleware完成。这意味着即使子域可以在您的域上设置或修改cookie,它也不能强制用户发布到您的应用程序,因为该请求不会来自您自己的确切域。 这也解决了在使用会话独立秘密时在HTTPS下可能发生的中间人攻击,因为即使在HTTPS下与站点通信时,HTTP Set-Cookie标头(不幸)也被客户接受了。 。 (对HTTP请求不进行引用检查,因为在HTTP下,Referer头的存在不够可靠。) 如果设置了CSRF_COOKIE_DOMAIN设置,则会将引用者与其进行比较。此设置支持子域。例如,CSRF_COOKIE_DOMAIN ='.example.com'将允许来自www.example.comapi.example.com的POST请求。如果未设置该设置,则referer必须与HTTP Host标头匹配。 可以使用CSRF_TRUSTED_ORIGINS设置将已接受的引用扩展到当前主机或cookie域之外。

    流程图

    这里写图片描述

    CsrfViewMiddleware.process_request

    # django/middleware/csrf.py
    class CsrfViewMiddleware(MiddlewareMixin):
        def process_request(self, request):
            csrf_token = self._get_token(request)
            # 第一次访问,csrf_token返回None,
    
            if csrf_token is not None:
                # Use same token next time.
                request.META['CSRF_COOKIE'] = csrf_token
                # request.META 是一个 Python 字典,包含了所有本次 HTTP 请求的 Header
                # 信息,比如用户 IP 地址和用户Agent(通常是浏览器的名称和版本号)。

      settings = LazySettings()

    方法_get_token,从名字上来看就是获取token,_get_token在后面多处地方都有用到

    # django/middleware/csrf.py
    def _get_token(self, request):
        # CSRF_USE_SESSIONS在django/conf/global_settings.py,默认为False,执行else
        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]
                # CSRF_SESSION_KEY= "csrftoken"
            except KeyError:
                # 第一次访问的时候 request.COOKIES = {},所以直接返回
                return None
    
            csrf_token = _sanitize_token(cookie_token)
            # csrf 对不上 cookie里 的 token,标记csrf_cookie_needs_reset=True,
            # 在process_response的方法中判定
            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
    
    # /django/middleware/csrf.py
    
    CSRF_SECRET_LENGTH = 32
    CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH
    
    def _sanitize_token(token):
        # Allow only ASCII alphanumerics
        # 仅允许ASCII字母数字
        if re.search('[^a-zA-Z0-9]', token):
            return _get_new_csrf_token()
    

    先跳转到_get_new_csrf_token(),看他的生成方法

    def _get_new_csrf_token():
        return _salt_cipher_secret(_get_new_csrf_string())
    
    CSRF_SECRET_LENGTH = 32
    CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH
    
    def _get_new_csrf_string():
        return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS)
    
    
    def _salt_cipher_secret(secret):
        """
        Given a secret (assumed to be a string of CSRF_ALLOWED_CHARS), generate a
        token by adding a salt and using it to encrypt the secret.
    
        给定一个secret(假设是一串CSRF_ALLOWED_CHARS),通过添加一个随机生成值并使用它来加
        密secret来生成一个token。
    
        """
        salt = _get_new_csrf_string()
        chars = CSRF_ALLOWED_CHARS
        pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in salt))
        cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs)
        return salt + cipher
    # django/utils/crypto.py
    def get_random_string(length=12,
                          allowed_chars='abcdefghijklmnopqrstuvwxyz'
                                        'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
        """
        Return a securely generated random string.
        返回安全生成的随机字符串。
    
        The default length of 12 with the a-z, A-Z, 0-9 character set returns
        a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
        """
        if not using_sysrandom:
            # This is ugly, and a hack, but it makes things better than
            # the alternative of predictability. This re-seeds the PRNG
            # using a value that is hard for an attacker to predict, every
            # time a random string is required. This may change the
            # properties of the chosen random sequence slightly, but this
            # is better than absolute predictability.
            random.seed(
                hashlib.sha256(
                    ('%s%s%s' % (random.getstate(), time.time(), settings.SECRET_KEY)).encode()
                ).digest()
            )
        return ''.join(random.choice(allowed_chars) for i in range(length))

    返回的是一个随机的字符串

        # 接上面 def _sanitize_token
        elif len(token) == CSRF_TOKEN_LENGTH:
            return token
        elif len(token) == CSRF_SECRET_LENGTH:
            # Older Django versions set cookies to values of CSRF_SECRET_LENGTH
            # alphanumeric characters. For backwards compatibility, accept
            # such values as unsalted secrets.
            # It's easier to salt here and be consistent later, rather than add
            # different code paths in the checks, although that might be a tad more
            # efficient.
    
            # 较旧的Django版本将cookie设置为CSRF_SECRET_LENGTH字母数字字符的值。 为了向后
            # 兼容,接受诸如无保密秘密之类的值。这里更容易加盐并在以后保持一致,而不是在检查
            # 中添加不同的代码路径,尽管这可能会更有效。
            return _salt_cipher_secret(token)
        return _get_new_csrf_token()

    CsrfViewMiddleware.process_view

    # django/middleware/csrf.py
    class CsrfViewMiddleware(MiddlewareMixin):
        def process_view(self, request, callback, callback_args, callback_kwargs):
            if getattr(request, 'csrf_processing_done', False):
                return None
    
            # Wait until request.META["CSRF_COOKIE"] has been manipulated before
            # bailing out, so that get_token still works
    
            # 如果装饰器 @csrf_exempt 生效,则不处理
            if getattr(callback, 'csrf_exempt', False):
                return None
    
            # Assume that anything not defined as 'safe' by RFC7231 needs protection
            if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
                if getattr(request, '_dont_enforce_csrf_checks', False):
                    # Mechanism to turn off CSRF checks for test suite.
                    # It comes after the creation of CSRF cookies, so that
                    # everything else continues to work exactly the same
                    # (e.g. cookies are sent, etc.), but before any
                    # branches that call reject().
    
                    # 关闭CSRF检查测试套件的机制。在创建CSRF cookie之后,所以
                    # 其他所有内容继续完全相同(例如发送cookie等),但在调用
                    # reject()的任何分支之前。
    
                    return self._accept(request)
    

        def _accept(self, request):

            # Avoid checking the request twice by adding a custom attribute to
            # request.  This will be relevant when both decorator and middleware
            # are used.
            request.csrf_processing_done = True
            return None

    接上面CsrfViewMiddleware.process_view的代码

                # is_secure 如果请求是安全的,返回True,意味着发出的是HTTPS请求。
                if request.is_secure():
                    referer = request.META.get('HTTP_REFERER')
                    if referer is None:
                        return self._reject(request, REASON_NO_REFERER)
                        # _reject就是csrf验证不通过,因为reffer为空

    返回一个丑拒的代码

        def _reject(self, request, reason):
            logger.warning(
                'Forbidden (%s): %s', reason, request.path,
                extra={
                    'status_code': 403,
                    'request': request,
                }
            )
            return _get_failure_view()(request, reason=reason)
                    referer = urlparse(referer)
    
                    # referer.scheme: 请求的协议,一般为http或者https
                    # referer.netloc: host域名
    
                    # 确保我们有一个有效的url在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)
    
                    # If there isn't a CSRF_COOKIE_DOMAIN, require an exact match
                    # match on host:port. If not, obey the cookie rules (or those
                    # for the session cookie, if CSRF_USE_SESSIONS).
                    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:
                        # request.get_host() includes the port.
                        good_referer = request.get_host()
    
                    # 在这里,我们生成所有可接受的HTTP引用的列表,包括当前主机,因
                    # 为它已在上游验证。
                    # CSRF_TRUSTED_ORIGINS global_settings.py里为空的list,设置可
                    # 以信任的来源
                    good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
                    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:
                    # 没有CSRF cookie。对于POST请求,我们坚持使用CSRF 
                    # cookie,这样我们就可以避免所有CSRF攻击,包括登录CSRF。
                    return self._reject(request, REASON_NO_CSRF_COOKIE)
    
                # Check non-cookie token for match.
                request_csrf_token = ""
                if request.method == "POST":
                    try:
                        # request.POST.get() 相当于获取request.POST['csrfmiddlewaretoken']的值,
                        # 若果出错就返回 ''.这里的csrfmiddlewaretoken是提交的表单中的值,在
                        # 模板中用{% csrf_token %} 生成
                        request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
                    except IOError:
                        # Handle a broken connection before we've completed reading
                        # the POST data. process_view shouldn't raise any
                        # exceptions, so we'll ignore and serve the user a 403
                        # (assuming they're still listening, which they probably
                        # aren't because of the error).
    
                        # 在我们完成读取POST数据之前处理断开的连接。   
                        # process_view不应该引发任何exception,因此我们将忽略并返回403
                        #(假设他们仍在监听,他们可能不是因为错误)。
    
                        pass
    
                if request_csrf_token == "":
                    # Fall back to X-CSRFToken, to make things easier for AJAX,
                    # and possible for PUT/DELETE.
                    # ajax中适用'X-CSRFToken'
                    # CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
                    request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
    
                request_csrf_token = _sanitize_token(request_csrf_token)
                # 对比两个csrf_token,一个是表单里隐藏的csrfmiddlewaretoken
                #(或者ajax的hearder: X_CSRFTOKEN),另一个是自带的cookies里的csrf_token
                if not _compare_salted_tokens(request_csrf_token, csrf_token):
                    # 匹配不对就拒绝
                    return self._reject(request, REASON_BAD_TOKEN)
    
            return self._accept(request)

      def _compare_salted_tokens(request_csrf_token, csrf_token):

        # Assume both arguments are sanitized -- that is, strings of
        # length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS.
        return constant_time_compare(
            _unsalt_cipher_token(request_csrf_token),
            _unsalt_cipher_token(csrf_token),
        )

    def _unsalt_cipher_token(token):
        """
        Given a token (assumed to be a string of CSRF_ALLOWED_CHARS, of length
        CSRF_TOKEN_LENGTH, and that its first half is a salt), use it to decrypt
        the second half to produce the original secret.
        """
        salt = token[:CSRF_SECRET_LENGTH]
        token = token[CSRF_SECRET_LENGTH:]
        chars = CSRF_ALLOWED_CHARS
        pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in salt))
        secret = ''.join(chars[x - y] for x, y in pairs)  # Note negative values are ok
        return secret
        def _accept(self, request):
            # Avoid checking the request twice by adding a custom attribute to
            # request.  This will be relevant when both decorator and middleware
            # are used.
            request.csrf_processing_done = True
            return None

    get_token(重要)

    get_token是在外部调用,由 Template 中的{% csrf_token %} 触发,由request的cookie不同做出不同的反应。

    def get_token(request):
         if "CSRF_COOKIE" not in request.META:
            # 如果request中不存在csrf,先生成一个新的secret,加密赋值到META["CSRF_COOKIE"] 中,
            # 后面用来放到set_cookie之中
            csrf_secret = _get_new_csrf_string()
            request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret)
       else:
        # 如果request的cookie中存在了csrf_token,冲洗解密,取出secret        csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
        request.META["CSRF_COOKIE_USED"] = True
        # 返回另外一个加密生成的secret, 由于加密是随机的,所以与上面的META["CSRF_COOKIE"]不一样
        return _salt_cipher_secret(csrf_secret)

    上面返回的一个加密的secret将会被填充进入 
    <input type="hidden" name="csrfmiddlewaretoken" value="{}" >value里面,随着表单一起提交并和cookie之中的csrf_token比较。

    CsrfViewMiddleware.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
    
            # Set the CSRF cookie even if it's already set, so we renew
            # the expiry timer.
            self._set_token(request, response)
            response.csrf_cookie_set = True
            return response
        # 设置token
        def _set_token(self, request, response):
            if settings.CSRF_USE_SESSIONS:
                request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
            else:
                response.set_cookie(
                    settings.CSRF_COOKIE_NAME,
                     # request.META['CSRF_COOKIE']就是在上面赋值的
                    request.META['CSRF_COOKIE'],
                    max_age=settings.CSRF_COOKIE_AGE,
                    domain=settings.CSRF_COOKIE_DOMAIN,
                    path=settings.CSRF_COOKIE_PATH,
                    secure=settings.CSRF_COOKIE_SECURE,
                    httponly=settings.CSRF_COOKIE_HTTPONLY,
                )
                # Set the Vary header since content varies with the CSRF cookie.
                patch_vary_headers(response, ('Cookie',))

    总结

    • 第一次访问页面 
      • 首先第一次访问页面,Template中的{% csrf_token %}会启动get_token(不是私有方法_get_token),生产一个csrf_secret的值。
      • 这个值在_salt_cipher_secret中随机生产一个与csrf_secret长度相同的salt,利用salt加密csrf_secret,两个字符串拼接形成csrf_token,request.META['CSRF_COOKIE'] = csrf_token 并设置到cookie里面。
      • get_token返回的用随机生成的另外一个salt加密csrf_secret,同样拼接返回放入隐藏的input之中
    • 向页面提交表单 
      • 提交的cookie中含有的csrf_token与表单提交的csrfmiddlewaretokenprocess_view进行解密,比对,如果解密出来的数值不同直接返回_reject() 
        这里写图片描述
  • 相关阅读:
    c++ STL中的vector与list为什么没有提供find操作?
    转发:CAOZ星球提问。 遇到很大瓶颈,想离职又不敢离职怎么办
    转发 :caoz:数据分析这点事
    那些绕不开的Linux
    记录 《 Bootstrap 基础教程》 学习笔记 第一天
    迈出你的第一步——天助自助者
    this指向问题
    小结
    前端小白的福利
    真实案例分享
  • 原文地址:https://www.cnblogs.com/ellisonzhang/p/10695121.html
Copyright © 2011-2022 走看看