1. 程式人生 > >django中介軟體CsrfViewMiddleware原始碼分析,探究csrf實現

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_tokenrequest.META['CSRF_COOKIE'] = csrf_token 並設定到cookie裡面。
    • get_token返回的用隨機生成的另外一個salt加密csrf_secret,同樣拼接返回放入隱藏的input之中
  • 向頁面提交表單
    • 提交的cookie中含有的csrf_token與表單提交的csrfmiddlewaretokenprocess_view進行解密,比對,如果解密出來的數值不同直接返回_reject()
      這裡寫圖片描述

參考資料