Django中间件CsrfViewMiddleware源码分析

Django Documentation

csrf保护基于以下:

1, 一个CSRF cookie基于一个随机生成的值,其他网站无法得到,次cookie有CsrfViewMiddleware产生.它与每个调用django.middleware.csrf.get_token()(这是一个用于取回CSRF token的方法)的响应一起发送,如果它尚未在请求上设置的话.

为了放置BREACH攻击,token不仅仅是比吗,随机的salt被置于secret之前并用来加密它,出于安全原因,每次用户登陆都会更改密钥的值.

CsrfViewMiddleware.process_request

class CsrfViewMiddleware(MiddlewareMixin):
    def process_request(self, request):
        csrf_token = self._get_token(request)
        # 第一次访问, csrf_token返回None,

        if csrf_token is not None:
            request.META["CSRF_COOKIE"] = csrf_token
            # request.META是一个python字典,包含了所有本次Http请求的Header信息,比如用户IP地址和用户Agent(通常是浏览器的名称版本号).
settings = Lazysettins()

这是一个懒加载

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

    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
CSRF_SECRET_LENGTH = 32
CSRF_TOKEN_LENGTH = 2 * CSRF_SECRET_LENGTH

def _sanitize_token(token):
    # Allow only ASCII alphanumerics
    if re.search(‘[^a-zA-Z0-9]‘, force_text(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‘):
    """
    Returns 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(‘utf-8‘)
            ).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()
    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为空

返回一个Forbidden的代码

    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域名

                # Make sure we have a valid URL for Referer.
                # 确保我们在referer中有一个有效的URL
                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()

                # Here we generate a list of all acceptable HTTP referers,
                # including the current host since that has been validated
                # upstream.

                # 在这里,我们生成所有可能接受HTTP引用的列表,包括当前主机,因为
                # 它已经在上游验证.
                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:
                # No CSRF cookie. For POST requests, we insist on a CSRF cookie,
                # and in this way we can avoid all CSRF attacks, including login
                # 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"
                # CERF_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的header: 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(important)

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

def get_token(request):
    """
    Returns the CSRF token required for a POST form. The token is an
    alphanumeric value. A new token is created if one is not already set.

    A side effect of calling this function is to make the csrf_protect
    decorator and the CsrfViewMiddleware add a CSRF cookie and a ‘Vary: Cookie‘
    header to the outgoing response.  For this reason, you may need to use this
    function lazily, as is done by the csrf context processor.
    """
    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:
        csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
    request.META["CSRF_COOKIE_USED"] = True
    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(不是私有方法), 产生一个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,同样拼接返回放入process_view进行解密,比对,如果解密出来的数值不同直接返回_reject()

  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域之外。

原文地址:https://www.cnblogs.com/chenrun/p/9676010.html

时间: 2024-10-19 20:02:24

Django中间件CsrfViewMiddleware源码分析的相关文章

Django搭建及源码分析(三)---+uWSGI+nginx

每个框架或者应用都是为了解决某些问题才出现旦生的,没有一个事物是可以解决所有问题的.如果觉得某个框架或者应用使用很不方便,那么很有可能就是你没有将其使用到正确的地方,没有按开发者的设计初衷来使用它,当你将一个框架的优势使用到极致时一定是非常舒服和顺手的一件事.但同时也有可能衍生另一个问题,这个框架只解决了你的问题一,没有解决问题二.三等等,因此,就出现了多个框架/应用相结合的情况.比如Django + uWSGI + nginx. 本人初学python,找了一些实例进行了一些操作,以下纯属目前的

Django如何启动源码分析

Django如何启动源码分析 启动 我们启动Django是通过python manage.py runsever的命令 解决 这句话就是执行manage.py文件,并在命令行发送一个runsever字符串 解析manage.py #!/usr/bin/env python import os import sys if __name__ == "__main__": #os.environ.setdefault 方法可以修改系统环境变量,但是只能os.environ 只能影响到当前运行

Django rest framework源码分析(一) 认证

一.基础 最近正好有机会去写一些可视化的东西,就想着前后端分离,想使用django rest framework写一些,顺便复习一下django rest framework的知识,只是顺便哦,好吧.我承认我是故意的,因为我始终觉得,如果好的技术服务于企业,顺便的提高一下自己.大家都很开心不是不.再次强调一下,真的只是顺便. 安装吧 pip install djangorestframework 1.2.需要先了解的一些知识 理解下面两个知识点非常重要,django-rest-framework

Django框架 --CBV源码分析、restful规范、restframework框架

一.CBV源码分析 1.url层的使用CBV from app01 import views url(r'book/',views.Book.as_view()) 2.as_view方法 as_view是一个类方法,实际上是一个闭包函数(内层函数包含对外层作用域的使用) 请求来了以后,调用as_view方法,调用函数中的view方法,view方法是调用了dispatch方法 @classonlymethod def as_view(cls, **initkwargs): def view(req

Django 之restfromwork 源码分析之--视图组件

restframework 源码分析以及使用 mixins 中的五种类方法 from rest_framework import mixins # mixins 中一种有五种类 # 第一种:用户保存数据 class CreateModelMixin(object): """ Create a model instance. """ def create(self, request, *args, **kwargs): # 序列化的类的对象 ser

Django搭建及源码分析(二)

本节从由Django生成的manage.py开始,分析Django源码.python版本2.6,Django版本1.6.11. manage.py代码很简单. #!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "MyDjProj.settings") from d

数据库分库分表中间件 Sharding-JDBC 源码分析 —— 分布式主键

关注**微信公众号:[芋道源码]**有福利: RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表 RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址 您对于源码的疑问每条留言都将得到认真回复.甚至不知道如何读源码也可以请教噢. 新的源码解析文章实时收到通知.每周更新一篇左右. 认真的源码交流微信群. 本文主要基于 Sharding-JDBC 1.5.0 正式版 1. 概述 2.KeyGenerator 2.1 D

数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析(一)之语法解析

关注微信公众号:[芋艿的后端小屋]有福利: RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表 RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址 您对于源码的疑问每条留言都将得到认真回复.甚至不知道如何读源码也可以请教噢. 新的源码解析文章实时收到通知.每周更新一篇左右. 1. 概述 2. Lexer 词法解析器 3. Token 词法标记 3.2.1 Literals.IDENTIFIER 词法关键词 3.2

django中CBV源码分析

前言:Django的视图处理方式有两种: FBV(function base views) 是在视图里基于函数形式处理请求. CBV(class base views)是在视图里基于类的形式处理请求. Python是一个面向对象的编程语言,如果只用函数来开发,有很多面向对象的优点就错失了(继承.封装.多态).所以Django在后来加入了Class-Based-View.可以让我们用类写View.这样做的优点主要下面两种: 提高了代码的复用性,可以使用面向对象的技术,比如Mixin(多继承) 可以