Fixed #27863 -- Added support for the SameSite cookie flag.
Thanks Alex Gaynor for contributing to the patch.
This commit is contained in:
parent
13efbb233a
commit
9a56b4b13e
|
@ -461,6 +461,9 @@ SESSION_COOKIE_SECURE = False
|
|||
SESSION_COOKIE_PATH = '/'
|
||||
# Whether to use the non-RFC standard httpOnly flag (IE, FF3+, others)
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
# Whether to set the flag restricting cookie leaks on cross-site requests.
|
||||
# This can be 'Lax', 'Strict', or None to disable the flag.
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
# Whether to save the session data on every request.
|
||||
SESSION_SAVE_EVERY_REQUEST = False
|
||||
# Whether a user's session cookie expires when the Web browser is closed.
|
||||
|
@ -537,6 +540,7 @@ CSRF_COOKIE_DOMAIN = None
|
|||
CSRF_COOKIE_PATH = '/'
|
||||
CSRF_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_HTTPONLY = False
|
||||
CSRF_COOKIE_SAMESITE = 'Lax'
|
||||
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
|
||||
CSRF_TRUSTED_ORIGINS = []
|
||||
CSRF_USE_SESSIONS = False
|
||||
|
|
|
@ -86,6 +86,7 @@ class CookieStorage(BaseStorage):
|
|||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
secure=settings.SESSION_COOKIE_SECURE or None,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
|
||||
samesite=settings.SESSION_COOKIE_SAMESITE,
|
||||
)
|
||||
else:
|
||||
response.delete_cookie(self.cookie_name, domain=settings.SESSION_COOKIE_DOMAIN)
|
||||
|
|
|
@ -69,5 +69,6 @@ class SessionMiddleware(MiddlewareMixin):
|
|||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=settings.SESSION_COOKIE_SECURE or None,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
|
||||
samesite=settings.SESSION_COOKIE_SAMESITE,
|
||||
)
|
||||
return response
|
||||
|
|
|
@ -3,6 +3,9 @@ from http import cookies
|
|||
# For backwards compatibility in Django 2.1.
|
||||
SimpleCookie = cookies.SimpleCookie
|
||||
|
||||
# Add support for the SameSite attribute (obsolete when PY37 is unsupported).
|
||||
cookies.Morsel._reserved.setdefault('samesite', 'SameSite')
|
||||
|
||||
|
||||
def parse_cookie(cookie):
|
||||
"""
|
||||
|
|
|
@ -154,7 +154,7 @@ class HttpResponseBase:
|
|||
return self._headers.get(header.lower(), (None, alternate))[1]
|
||||
|
||||
def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
|
||||
domain=None, secure=False, httponly=False):
|
||||
domain=None, secure=False, httponly=False, samesite=None):
|
||||
"""
|
||||
Set a cookie.
|
||||
|
||||
|
@ -194,6 +194,10 @@ class HttpResponseBase:
|
|||
self.cookies[key]['secure'] = True
|
||||
if httponly:
|
||||
self.cookies[key]['httponly'] = True
|
||||
if samesite:
|
||||
if samesite.lower() not in ('lax', 'strict'):
|
||||
raise ValueError('samesite must be "lax" or "strict".')
|
||||
self.cookies[key]['samesite'] = samesite
|
||||
|
||||
def setdefault(self, key, value):
|
||||
"""Set a header unless it has already been set."""
|
||||
|
|
|
@ -190,6 +190,7 @@ class CsrfViewMiddleware(MiddlewareMixin):
|
|||
path=settings.CSRF_COOKIE_PATH,
|
||||
secure=settings.CSRF_COOKIE_SECURE,
|
||||
httponly=settings.CSRF_COOKIE_HTTPONLY,
|
||||
samesite=settings.CSRF_COOKIE_SAMESITE,
|
||||
)
|
||||
# Set the Vary header since content varies with the CSRF cookie.
|
||||
patch_vary_headers(response, ('Cookie',))
|
||||
|
|
|
@ -513,6 +513,7 @@ A number of settings can be used to control Django's CSRF behavior:
|
|||
* :setting:`CSRF_COOKIE_HTTPONLY`
|
||||
* :setting:`CSRF_COOKIE_NAME`
|
||||
* :setting:`CSRF_COOKIE_PATH`
|
||||
* :setting:`CSRF_COOKIE_SAMESITE`
|
||||
* :setting:`CSRF_COOKIE_SECURE`
|
||||
* :setting:`CSRF_FAILURE_VIEW`
|
||||
* :setting:`CSRF_HEADER_NAME`
|
||||
|
|
|
@ -748,7 +748,7 @@ Methods
|
|||
|
||||
Sets a header unless it has already been set.
|
||||
|
||||
.. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False)
|
||||
.. method:: HttpResponse.set_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=False, samesite=None)
|
||||
|
||||
Sets a cookie. The parameters are the same as in the
|
||||
:class:`~http.cookies.Morsel` cookie object in the Python standard library.
|
||||
|
@ -773,8 +773,17 @@ Methods
|
|||
when it is honored, it can be a useful way to mitigate the
|
||||
risk of a client-side script from accessing the protected cookie
|
||||
data.
|
||||
* Use ``samesite='Strict'`` or ``samesite='Lax'`` to tell the browser not
|
||||
to send this cookie when performing a cross-origin request. `SameSite`_
|
||||
isn't supported by all browsers, so it's not a replacement for Django's
|
||||
CSRF protection, but rather a defense in depth measure.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
The ``samesite`` argument was added.
|
||||
|
||||
.. _HTTPOnly: https://www.owasp.org/index.php/HTTPOnly
|
||||
.. _SameSite: https://www.owasp.org/index.php/SameSite
|
||||
|
||||
.. warning::
|
||||
|
||||
|
@ -784,7 +793,7 @@ Methods
|
|||
to store a cookie of more than 4096 bytes, but many browsers will not
|
||||
set the cookie correctly.
|
||||
|
||||
.. method:: HttpResponse.set_signed_cookie(key, value, salt='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=True)
|
||||
.. method:: HttpResponse.set_signed_cookie(key, value, salt='', max_age=None, expires=None, path='/', domain=None, secure=None, httponly=True, samesite=None)
|
||||
|
||||
Like :meth:`~HttpResponse.set_cookie()`, but
|
||||
:doc:`cryptographic signing </topics/signing>` the cookie before setting
|
||||
|
|
|
@ -365,6 +365,20 @@ This is useful if you have multiple Django instances running under the same
|
|||
hostname. They can use different cookie paths, and each instance will only see
|
||||
its own CSRF cookie.
|
||||
|
||||
.. setting:: CSRF_COOKIE_SAMESITE
|
||||
|
||||
``CSRF_COOKIE_SAMESITE``
|
||||
------------------------
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
Default: ``'Lax'``
|
||||
|
||||
The value of the `SameSite`_ flag on the CSRF cookie. This flag prevents the
|
||||
cookie from being sent in cross-site requests.
|
||||
|
||||
See :setting:`SESSION_COOKIE_SAMESITE` for details about ``SameSite``.
|
||||
|
||||
.. setting:: CSRF_COOKIE_SECURE
|
||||
|
||||
``CSRF_COOKIE_SECURE``
|
||||
|
@ -3025,6 +3039,44 @@ This is useful if you have multiple Django instances running under the same
|
|||
hostname. They can use different cookie paths, and each instance will only see
|
||||
its own session cookie.
|
||||
|
||||
.. setting:: SESSION_COOKIE_SAMESITE
|
||||
|
||||
``SESSION_COOKIE_SAMESITE``
|
||||
---------------------------
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
Default: ``'Lax'``
|
||||
|
||||
The value of the `SameSite`_ flag on the session cookie. This flag prevents the
|
||||
cookie from being sent in cross-site requests thus preventing CSRF attacks and
|
||||
making some methods of stealing session cookie impossible.
|
||||
|
||||
Possible values for the setting are:
|
||||
|
||||
* ``'Strict'``: prevents the cookie from being sent by the browser to the
|
||||
target site in all cross-site browsing context, even when following a regular
|
||||
link.
|
||||
|
||||
For example, for a GitHub-like website this would mean that if a logged-in
|
||||
user follows a link to a private GitHub project posted on a corporate
|
||||
discussion forum or email, GitHub will not receive the session cookie and the
|
||||
user won't be able to access the project. A bank website, however, most
|
||||
likely doesn't want to allow any transactional pages to be linked from
|
||||
external sites so the ``'Strict'`` flag would be appropriate.
|
||||
|
||||
* ``'Lax'`` (default): provides a balance between security and usability for
|
||||
websites that want to maintain user's logged-in session after the user
|
||||
arrives from an external link.
|
||||
|
||||
In the GitHub scenario, the session cookie would be allowed when following a
|
||||
regular link from an external website and be blocked in CSRF-prone request
|
||||
methods (e.g. ``POST``).
|
||||
|
||||
* ``None``: disables the flag.
|
||||
|
||||
.. _SameSite: https://www.owasp.org/index.php/SameSite
|
||||
|
||||
.. setting:: SESSION_COOKIE_SECURE
|
||||
|
||||
``SESSION_COOKIE_SECURE``
|
||||
|
@ -3425,6 +3477,7 @@ Security
|
|||
* :setting:`CSRF_COOKIE_DOMAIN`
|
||||
* :setting:`CSRF_COOKIE_NAME`
|
||||
* :setting:`CSRF_COOKIE_PATH`
|
||||
* :setting:`CSRF_COOKIE_SAMESITE`
|
||||
* :setting:`CSRF_COOKIE_SECURE`
|
||||
* :setting:`CSRF_FAILURE_VIEW`
|
||||
* :setting:`CSRF_HEADER_NAME`
|
||||
|
|
|
@ -112,7 +112,8 @@ Minor features
|
|||
:mod:`django.contrib.sessions`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
* Added the :setting:`SESSION_COOKIE_SAMESITE` setting to set the ``SameSite``
|
||||
cookie flag on session cookies.
|
||||
|
||||
:mod:`django.contrib.sitemaps`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -143,7 +144,8 @@ Cache
|
|||
CSRF
|
||||
~~~~
|
||||
|
||||
* ...
|
||||
* Added the :setting:`CSRF_COOKIE_SAMESITE` setting to set the ``SameSite``
|
||||
cookie flag on CSRF cookies.
|
||||
|
||||
Database backends
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
@ -239,6 +241,9 @@ Requests and Responses
|
|||
|
||||
* Added :meth:`.HttpRequest.get_full_path_info`.
|
||||
|
||||
* Added the ``samesite`` argument to :meth:`.HttpResponse.set_cookie` to allow
|
||||
setting the ``SameSite`` cookie flag.
|
||||
|
||||
Serialization
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
|
@ -338,6 +343,16 @@ variable now appears as an attribute of each option. For example, in a custom
|
|||
``input_option.html`` template, change ``{% if wrap_label %}`` to
|
||||
``{% if widget.wrap_label %}``.
|
||||
|
||||
``SameSite`` cookies
|
||||
--------------------
|
||||
|
||||
The cookies used for ``django.contrib.sessions``, ``django.contrib.messages``,
|
||||
and Django's CSRF protection now set the ``SameSite`` flag to ``Lax`` by
|
||||
default. Browsers that respect this flag won't send these cookies on
|
||||
cross-origin requests. If you rely on the old behavior, set the
|
||||
:setting:`SESSION_COOKIE_SAMESITE` and/or :setting:`CSRF_COOKIE_SAMESITE`
|
||||
setting to ``None``.
|
||||
|
||||
Miscellaneous
|
||||
-------------
|
||||
|
||||
|
|
|
@ -629,6 +629,7 @@ behavior:
|
|||
* :setting:`SESSION_COOKIE_HTTPONLY`
|
||||
* :setting:`SESSION_COOKIE_NAME`
|
||||
* :setting:`SESSION_COOKIE_PATH`
|
||||
* :setting:`SESSION_COOKIE_SAMESITE`
|
||||
* :setting:`SESSION_COOKIE_SECURE`
|
||||
* :setting:`SESSION_ENGINE`
|
||||
* :setting:`SESSION_EXPIRE_AT_BROWSER_CLOSE`
|
||||
|
|
|
@ -586,6 +586,14 @@ class CsrfViewMiddlewareTests(CsrfViewMiddlewareTestMixin, SimpleTestCase):
|
|||
max_age = resp2.cookies.get('csrfcookie').get('max-age')
|
||||
self.assertEqual(max_age, '')
|
||||
|
||||
def test_csrf_cookie_samesite(self):
|
||||
req = self._get_GET_no_csrf_cookie_request()
|
||||
with self.settings(CSRF_COOKIE_NAME='csrfcookie', CSRF_COOKIE_SAMESITE='Strict'):
|
||||
self.mw.process_view(req, token_view, (), {})
|
||||
resp = token_view(req)
|
||||
resp2 = self.mw.process_response(req, resp)
|
||||
self.assertEqual(resp2.cookies['csrfcookie']['samesite'], 'Strict')
|
||||
|
||||
def test_process_view_token_too_long(self):
|
||||
"""
|
||||
If the token is longer than expected, it is ignored and a new token is
|
||||
|
|
|
@ -746,6 +746,11 @@ class CookieTests(unittest.TestCase):
|
|||
# document.cookie parses whitespace.
|
||||
self.assertEqual(parse_cookie(' = b ; ; = ; c = ; '), {'': 'b', 'c': ''})
|
||||
|
||||
def test_samesite(self):
|
||||
c = SimpleCookie('name=value; samesite=lax; httponly')
|
||||
self.assertEqual(c['name']['samesite'], 'lax')
|
||||
self.assertIn('SameSite=lax', c.output())
|
||||
|
||||
def test_httponly_after_load(self):
|
||||
c = SimpleCookie()
|
||||
c.load("name=val")
|
||||
|
|
|
@ -57,6 +57,7 @@ class CookieTests(BaseTests, SimpleTestCase):
|
|||
# The message contains what's expected.
|
||||
self.assertEqual(list(storage), example_messages)
|
||||
|
||||
@override_settings(SESSION_COOKIE_SAMESITE='Strict')
|
||||
def test_cookie_setings(self):
|
||||
"""
|
||||
CookieStorage honors SESSION_COOKIE_DOMAIN, SESSION_COOKIE_SECURE, and
|
||||
|
@ -72,6 +73,7 @@ class CookieTests(BaseTests, SimpleTestCase):
|
|||
self.assertEqual(response.cookies['messages']['expires'], '')
|
||||
self.assertIs(response.cookies['messages']['secure'], True)
|
||||
self.assertIs(response.cookies['messages']['httponly'], True)
|
||||
self.assertEqual(response.cookies['messages']['samesite'], 'Strict')
|
||||
|
||||
# Test deletion of the cookie (storing with an empty value) after the messages have been consumed
|
||||
storage = self.get_storage()
|
||||
|
|
|
@ -79,6 +79,17 @@ class SetCookieTests(SimpleTestCase):
|
|||
response.set_cookie('test', cookie_value)
|
||||
self.assertEqual(response.cookies['test'].value, cookie_value)
|
||||
|
||||
def test_samesite(self):
|
||||
response = HttpResponse()
|
||||
response.set_cookie('example', samesite='Lax')
|
||||
self.assertEqual(response.cookies['example']['samesite'], 'Lax')
|
||||
response.set_cookie('example', samesite='strict')
|
||||
self.assertEqual(response.cookies['example']['samesite'], 'strict')
|
||||
|
||||
def test_invalid_samesite(self):
|
||||
with self.assertRaisesMessage(ValueError, 'samesite must be "lax" or "strict".'):
|
||||
HttpResponse().set_cookie('example', samesite='invalid')
|
||||
|
||||
|
||||
class DeleteCookieTests(SimpleTestCase):
|
||||
|
||||
|
|
|
@ -660,6 +660,16 @@ class SessionMiddlewareTests(TestCase):
|
|||
str(response.cookies[settings.SESSION_COOKIE_NAME])
|
||||
)
|
||||
|
||||
@override_settings(SESSION_COOKIE_SAMESITE='Strict')
|
||||
def test_samesite_session_cookie(self):
|
||||
request = RequestFactory().get('/')
|
||||
response = HttpResponse()
|
||||
middleware = SessionMiddleware()
|
||||
middleware.process_request(request)
|
||||
request.session['hello'] = 'world'
|
||||
response = middleware.process_response(request, response)
|
||||
self.assertEqual(response.cookies[settings.SESSION_COOKIE_NAME]['samesite'], 'Strict')
|
||||
|
||||
@override_settings(SESSION_COOKIE_HTTPONLY=False)
|
||||
def test_no_httponly_session_cookie(self):
|
||||
request = RequestFactory().get('/')
|
||||
|
|
Loading…
Reference in New Issue