csrf保護基於如下:
1. 一個CSRF cookie 基於一個隨機生成的值,其餘網站沒法獲得。此cookie由CsrfViewMiddleware
產生。它與每一個調用django.middleware.csrf.get_token()
(這是一個用於取回CSRF token的方法)的響應一塊兒發送,若是它還沒有在請求上設置的話。
爲了防止BREACH攻擊,token
不只僅是祕密;隨機的salt
被置於secret
以前並用來加密它。出於安全緣由,每次用戶登陸時都會更改密鑰的值。python
全部傳出POST表單中都有一個名爲csrfmiddlewaretoken
的隱藏表單字段。此字段的值一樣是祕密的值。salt添加到它並用於加擾它。每次調用get_token()時都會從新生成salt,以便在每一個此類響應中更改表單字段值。這部分由template的{% csrf_token %}
完成。ajax
對於未使用HTTP
GET
,HEAD
,OPTIONS
或TRACE
的全部傳入請求,必須帶有CSRF cookie
,而且csrfmiddlewaretoken
字段必須存在且正確。若是不是,用戶將收到403錯誤。
驗證csrfmiddlewaretoken
字段值時,只將secret而不是整個token與cookie值中的secret
進行比較。這容許使用不斷變化的token
。雖然每一個請求均可以使用本身的token
,但secret
仍然是全部人共同的。
此檢查由CsrfViewMiddleware
完成。django
此外,對於HTTPS
請求,嚴格的引用檢查由CsrfViewMiddleware
完成。這意味着即便子域能夠在您的域上設置或修改cookie
,它也不能強制用戶發佈到您的應用程序,由於該請求不會來自您本身的確切域。 這也解決了在使用會話獨立祕密時在HTTPS下可能發生的中間人攻擊,由於即便在HTTPS下與站點通訊時,HTTP Set-Cookie標頭(不幸)也被客戶接受了。 。 (對HTTP請求不進行引用檢查,由於在HTTP下,Referer頭的存在不夠可靠。) 若是設置了CSRF_COOKIE_DOMAIN
設置,則會將引用者與其進行比較。此設置支持子域。例如,CSRF_COOKIE_DOMAIN ='.example.com'
將容許來自www.example.com
和api.example.com
的POST請求。若是未設置該設置,則referer
必須與HTTP Host標頭匹配。 可使用CSRF_TRUSTED_ORIGINS
設置將已接受的引用擴展到當前主機或cookie域以外。api
# 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))
返回的是一個隨機的字符串markdown
# 接上面 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()
# 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):cookie
# 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
的代碼session
# 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
是在外部調用,由 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比較。
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',))
{% 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
之中csrfmiddlewaretoken
在process_view
進行解密,比對,若是解密出來的數值不一樣直接返回_reject()