#32800 closed Cleanup/optimization (fixed)
CsrfViewMiddleware unnecessarily masks CSRF cookie
Reported by: | Chris Jerdonek | Owned by: | Chris Jerdonek |
---|---|---|---|
Component: | CSRF | Version: | dev |
Severity: | Normal | Keywords: | |
Cc: | Shai Berger, Florian Apolloner | Triage Stage: | Ready for checkin |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description (last modified by )
I noticed that CsrfViewMiddleware
unnecessarily masks the CSRF cookie. See, for example:
https://github.com/django/django/blob/d270dd584e0af12fe6229fb712d0704c232dc7e5/django/middleware/csrf.py#L91
My understanding of the BREACH attack is that the vulnerability comes from not masking the CSRF token in the response body (e.g. what is included in the HTML). Masking the cookie itself doesn't help with this. (Django also doesn't change the mask of the cookie with each request, so the mask wouldn't help in this regard anyways.)
Some advantages of not masking the cookie are: It would simplify the code in CsrfViewMiddleware
because it would remove some complexity and operations that aren't needed for security. Currently, masking the CSRF cookie is a red herring for someone wanting to understand the various security features. Also, not masking the cookie would make requests and responses smaller when CSRF_USE_SESSIONS
is false or when true and cookie-based sessions are used. This is because masking doubles the length of the string.
This can be changed fairly easily while (1) continuing to respect masked cookie values, and (2) not forcing session stores to update their cookie in its unmasked form if they are currently storing it masked.
Change History (35)
comment:1 by , 3 years ago
Description: | modified (diff) |
---|
comment:2 by , 3 years ago
Cc: | added |
---|
comment:3 by , 3 years ago
comment:4 by , 3 years ago
Thanks for your comment! I'm pretty sure that scenario is already handled by the current code (I'm just not 100% sure if there are tests for it). For example, you can see in the code that _sanitize_token()
is called on the (non-cookie) CSRF token, and _sanitize_token()
has special logic to mask the cookie before returning it if it was passed the short version of length CSRF_SECRET_LENGTH
. (However, my preference is to put that logic in _compare_masked_tokens()
so that we're not relying on any assumptions on the arguments passed to _compare_masked_tokens()
like we currently are.). Note also that the code currently has backwards-compat logic to accept cookies that aren't masked (via the same call to _sanitize_token()
.)
FWIW, I did prepare a patch privately and saw no problems, and on the whole it seems slightly simpler. I even have a comment addressing the point you make:
# Fall back to X-CSRFToken, to make things easier for AJAX, and # possible for PUT/DELETE. try: # This can have length CSRF_SECRET_LENGTH or CSRF_TOKEN_LENGTH, # depending on whether the client obtained the token from # the DOM or the cookie (and whether the cookie was masked # or unmasked). request_csrf_token = request.META[settings.CSRF_HEADER_NAME] except KeyError: return self._reject(request, REASON_CSRF_TOKEN_MISSING)
comment:5 by , 3 years ago
Ok, then lets make sure we have that situation tested as well and I think we can go ahead with it.
comment:6 by , 3 years ago
Triage Stage: | Unreviewed → Accepted |
---|
comment:7 by , 3 years ago
Ok, then lets make sure we have that situation tested as well and I think we can go ahead with it.
I posted a preliminary PR for the purpose only of adding additional tests: https://github.com/django/django/pull/14485
The PR includes tests of all combinations of masked and unmasked secrets, and masked and unmasked tokens (both via POST
and the X-CSRFToken
header). See test_masked_unmasked_combinations()
in both CsrfViewMiddlewareTests
and CsrfViewMiddlewareUseSessionsTests
.
comment:8 by , 3 years ago
Has patch: | set |
---|
comment:9 by , 3 years ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
comment:10 by , 3 years ago
Has patch: | unset |
---|
comment:15 by , 3 years ago
comment:16 by , 3 years ago
PR: https://github.com/django/django/pull/14723
This PR adds more tests prior to the main code change. It also gives one function a better name, and in particular a name that will fit better following the change.
comment:17 by , 3 years ago
Has patch: | set |
---|
comment:20 by , 3 years ago
Has patch: | unset |
---|
comment:27 by , 3 years ago
Has patch: | unset |
---|
comment:28 by , 3 years ago
I posted a draft PR, FYI, just to get something up more quickly: https://github.com/django/django/pull/14776
I'll mark "has patch" when ready for review.
comment:32 by , 3 years ago
Triage Stage: | Accepted → Ready for checkin |
---|
I am a bit worried about backwards compat. What happens if JS reads the short unmasked secret from the Cookie and then sends that to the server (as opposed to reading it from the html)?