My understanding of the [http://breachattack.com/ 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
[https://github.com/django/django/blob/d270dd584e0af12fe6229fb712d0704c232dc7e5/django/middleware/csrf.py#L308-L309
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.
--
Ticket URL: <https://code.djangoproject.com/ticket/32800>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
Old description:
> 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 [http://breachattack.com/ 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
> [https://github.com/django/django/blob/d270dd584e0af12fe6229fb712d0704c232dc7e5/django/middleware/csrf.py#L308-L309
> 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.
New description:
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 [http://breachattack.com/ 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
[https://github.com/django/django/blob/d270dd584e0af12fe6229fb712d0704c232dc7e5/django/middleware/csrf.py#L308-L309
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.
--
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:1>
* cc: Shai Berger, Florian Apolloner (added)
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:2>
Comment (by Florian Apolloner):
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)?
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:3>
Comment (by Chris Jerdonek):
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
[https://github.com/django/django/blob/c609d5149c9295207cd7b2e8154e7b80a18d834a/django/middleware/csrf.py#L405-L409
in the code] that `_sanitize_token()` is called on the (non-cookie) CSRF
token, and `_sanitize_token()`
[https://github.com/django/django/blob/c609d5149c9295207cd7b2e8154e7b80a18d834a/django/middleware/csrf.py#L127-L134
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:
{{{#!python
# 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)
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:4>
Comment (by Florian Apolloner):
Ok, then lets make sure we have that situation tested as well and I think
we can go ahead with it.
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:5>
* stage: Unreviewed => Accepted
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:6>
Comment (by Chris Jerdonek):
> 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`.
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:7>
* has_patch: 0 => 1
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:8>
* owner: nobody => Chris Jerdonek
* status: new => assigned
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:9>
* has_patch: 1 => 0
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:10>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"c8108591b9868435e233ae9ff4289b280f451a4a" c8108591]:
{{{
#!CommitTicketReference repository=""
revision="c8108591b9868435e233ae9ff4289b280f451a4a"
Refs #32800 -- Added to csrf_tests/tests.py the unmasked version of the
secret.
This also adds tests that the secret is correct, and updates existing
tests to use the value.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:11>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"2523c32d50e3a34d7ad9e32a4150b6228b8b065c" 2523c32d]:
{{{
#!CommitTicketReference repository=""
revision="2523c32d50e3a34d7ad9e32a4150b6228b8b065c"
Refs #32800 -- Eliminated the need for separate _get_POST_bare_secret()
methods.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:12>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"defa8d3d87d5fcfd7675939b404ddc2bcdd13dcc" defa8d3]:
{{{
#!CommitTicketReference repository=""
revision="defa8d3d87d5fcfd7675939b404ddc2bcdd13dcc"
Refs #32800 -- Made CsrfViewMiddlewareTestMixin._csrf_id_cookie and
_csrf_id_token different.
This also renames CsrfViewMiddlewareTestMixin._csrf_id to _csrf_id_token.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:13>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"5e60c3943b04a674ef8687323930a0c7d5087c62" 5e60c394]:
{{{
#!CommitTicketReference repository=""
revision="5e60c3943b04a674ef8687323930a0c7d5087c62"
Refs #32800 -- Added CsrfViewMiddleware tests for all combinations of
masked/unmasked cookies and tokens.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:14>
Comment (by Chris Jerdonek):
I'm going to resolve #32902 before fixing the remainder of this ticket.
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:15>
Comment (by Chris Jerdonek):
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.
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:16>
* has_patch: 0 => 1
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:17>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"7132341255e725e15339a9cea0b494f6d576c688" 71323412]:
{{{
#!CommitTicketReference repository=""
revision="7132341255e725e15339a9cea0b494f6d576c688"
Refs #32800 -- Renamed _compare_masked_tokens() to _does_token_match().
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:18>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"795051b2b04020866fc14c858b77972ef8e5a3e2" 795051b]:
{{{
#!CommitTicketReference repository=""
revision="795051b2b04020866fc14c858b77972ef8e5a3e2"
Refs #32800 -- Added tests of more CSRF functions.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:19>
* has_patch: 1 => 0
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:20>
* has_patch: 0 => 1
Comment:
PR: https://github.com/django/django/pull/14761
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:21>
Comment (by Carlton Gibson <carlton@…>):
In [changeset:"7aba820aca98a3db77d405b616c9a2d39562f076" 7aba820a]:
{{{
#!CommitTicketReference repository=""
revision="7aba820aca98a3db77d405b616c9a2d39562f076"
Refs #32800 -- Improved
CsrfViewMiddlewareTestMixin._check_token_present().
This changes CsrfViewMiddlewareTestMixin._check_token_present() to give
more
detailed information if the check fails, and in particular why it failed.
It
also moves CsrfFunctionTests.assertMaskedSecretCorrect() to a separate
CsrfFunctionTestMixin so the helper can be used in
CsrfViewMiddlewareTestMixin.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:24>
Comment (by Carlton Gibson <carlton@…>):
In [changeset:"26d8e3f302adf0c12f64d614126e57147f8230d6" 26d8e3f]:
{{{
#!CommitTicketReference repository=""
revision="26d8e3f302adf0c12f64d614126e57147f8230d6"
Refs #32800 -- Used the cookie argument to
CsrfViewMiddlewareTestMixin._get_request() in more tests.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:22>
Comment (by Carlton Gibson <carlton@…>):
In [changeset:"f10553ec93b853029f3b3c6fe215648f48bd1223" f10553ec]:
{{{
#!CommitTicketReference repository=""
revision="f10553ec93b853029f3b3c6fe215648f48bd1223"
Refs #32800 -- Renamed _set_token() to _set_csrf_cookie().
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:23>
Comment (by Carlton Gibson <carlton@…>):
In [changeset:"231de683d86374c2b74da2185efc6ddfb5eb3341" 231de68]:
{{{
#!CommitTicketReference repository=""
revision="231de683d86374c2b74da2185efc6ddfb5eb3341"
Refs #32800 -- Added _add_new_csrf_cookie() helper function.
This centralizes the logic to use when setting a new cookie. It also
eliminates the need for the _get_new_csrf_token() function, which is now
removed.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:26>
Comment (by Carlton Gibson <carlton@…>):
In [changeset:"be1fd6645d4219b5c74152776e74d9e636b08554" be1fd66]:
{{{
#!CommitTicketReference repository=""
revision="be1fd6645d4219b5c74152776e74d9e636b08554"
Refs #32800 -- Added test_masked_secret_accepted_and_not_replaced().
This improves test_bare_secret_accepted_and_replaced() by adding a
stronger
assertion. It also adds a parallel test for the non-bare (masked) case.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:25>
* has_patch: 1 => 0
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:27>
Comment (by Chris Jerdonek):
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.
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:28>
* has_patch: 0 => 1
Comment:
PR: https://github.com/django/django/pull/14776
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:29>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"3f0025c18a08535ed39a64c24174f7e2d75b7b9e" 3f0025c1]:
{{{
#!CommitTicketReference repository=""
revision="3f0025c18a08535ed39a64c24174f7e2d75b7b9e"
Refs #32800 -- Avoided use of _does_token_match() in some CSRF tests.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:31>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"0820175d812e94850bc97a024c6cd7c29a94a10c" 0820175]:
{{{
#!CommitTicketReference repository=""
revision="0820175d812e94850bc97a024c6cd7c29a94a10c"
Refs #32800 -- Added CSRF tests for masked and unmasked secrets during
GET.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:30>
* stage: Accepted => Ready for checkin
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:32>
* status: assigned => closed
* resolution: => fixed
Comment:
In [changeset:"5d80843ebc5376d00f98bf2a6aadbada4c29365c" 5d80843e]:
{{{
#!CommitTicketReference repository=""
revision="5d80843ebc5376d00f98bf2a6aadbada4c29365c"
Fixed #32800 -- Changed CsrfViewMiddleware not to mask the CSRF secret.
This also adds CSRF_COOKIE_MASKED transitional setting helpful in
migrating multiple instance of the same project to Django 4.1+.
Thanks Florian Apolloner and Shai Berger for reviews.
Co-Authored-By: Mariusz Felisiak <felisiak...@gmail.com>
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:33>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"3ff7f6cf07a722635d690785c31ac89484134bee" 3ff7f6cf]:
{{{
#!CommitTicketReference repository=""
revision="3ff7f6cf07a722635d690785c31ac89484134bee"
Refs #32800 -- Renamed _sanitize_token() to _check_token_format().
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:34>
Comment (by Mariusz Felisiak <felisiak.mariusz@…>):
In [changeset:"e01970e9d23a241473671ea26126f8440db4dead" e01970e]:
{{{
#!CommitTicketReference repository=""
revision="e01970e9d23a241473671ea26126f8440db4dead"
Refs #32800 -- Removed CSRF_COOKIE_MASKED transitional setting per
deprecation timeline.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/32800#comment:35>