Browse Source

Merge pull request #2607 from FadhelC/SameSite-cookie-feature

Added support for cookie SameSite attribute
pull/2608/merge
David Lord 7 years ago committed by GitHub
parent
commit
e21abd9da5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      docs/config.rst
  2. 16
      docs/security.rst
  3. 1
      flask/app.py
  4. 11
      flask/sessions.py
  5. 29
      tests/test_basic.py
  6. 2
      tox.ini

15
docs/config.rst

@ -208,6 +208,16 @@ The following configuration values are used internally by Flask:
Default: ``False`` Default: ``False``
.. py:data:: SESSION_COOKIE_SAMESITE
Restrict how cookies are sent with requests from external sites. Can
be set to ``'Lax'`` (recommended) or ``'Strict'``.
See :ref:`security-cookie`.
Default: ``None``
.. versionadded:: 1.0
.. py:data:: PERMANENT_SESSION_LIFETIME .. py:data:: PERMANENT_SESSION_LIFETIME
If ``session.permanent`` is true, the cookie's expiration will be set this If ``session.permanent`` is true, the cookie's expiration will be set this
@ -361,13 +371,15 @@ The following configuration values are used internally by Flask:
``LOGGER_HANDLER_POLICY``, ``EXPLAIN_TEMPLATE_LOADING`` ``LOGGER_HANDLER_POLICY``, ``EXPLAIN_TEMPLATE_LOADING``
.. versionchanged:: 1.0 .. versionchanged:: 1.0
``LOGGER_NAME`` and ``LOGGER_HANDLER_POLICY`` were removed. See ``LOGGER_NAME`` and ``LOGGER_HANDLER_POLICY`` were removed. See
:ref:`logging` for information about configuration. :ref:`logging` for information about configuration.
Added :data:`ENV` to reflect the :envvar:`FLASK_ENV` environment Added :data:`ENV` to reflect the :envvar:`FLASK_ENV` environment
variable. variable.
Added :data:`SESSION_COOKIE_SAMESITE` to control the session
cookie's ``SameSite`` option.
Configuring from Files Configuring from Files
---------------------- ----------------------
@ -635,4 +647,3 @@ Example usage for both::
# or via open_instance_resource: # or via open_instance_resource:
with app.open_instance_resource('application.cfg') as f: with app.open_instance_resource('application.cfg') as f:
config = f.read() config = f.read()

16
docs/security.rst

@ -184,6 +184,9 @@ contains the same data. ::
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
.. _security-cookie:
Set-Cookie options Set-Cookie options
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
@ -194,17 +197,21 @@ They can be set on other cookies too.
- ``Secure`` limits cookies to HTTPS traffic only. - ``Secure`` limits cookies to HTTPS traffic only.
- ``HttpOnly`` protects the contents of cookies from being read with - ``HttpOnly`` protects the contents of cookies from being read with
JavaScript. JavaScript.
- ``SameSite`` ensures that cookies can only be requested from the same - ``SameSite`` restricts how cookies are sent with requests from
domain that created them. It is not supported by Flask yet. external sites. Can be set to ``'Lax'`` (recommended) or ``'Strict'``.
``Lax`` prevents sending cookies with CSRF-prone requests from
external sites, such as submitting a form. ``Strict`` prevents sending
cookies with all external requests, including following regular links.
:: ::
app.config.update( app.config.update(
SESSION_COOKIE_SECURE=True, SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Lax',
) )
response.set_cookie('username', 'flask', secure=True, httponly=True) response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax')
Specifying ``Expires`` or ``Max-Age`` options, will remove the cookie after Specifying ``Expires`` or ``Max-Age`` options, will remove the cookie after
the given time, or the current time plus the age, respectively. If neither the given time, or the current time plus the age, respectively. If neither
@ -237,6 +244,9 @@ values (or any values that need secure signatures).
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies - https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
.. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute
HTTP Public Key Pinning (HPKP) HTTP Public Key Pinning (HPKP)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1
flask/app.py

@ -284,6 +284,7 @@ class Flask(_PackageBoundObject):
'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_PATH': None,
'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SECURE': False,
'SESSION_COOKIE_SAMESITE': None,
'SESSION_REFRESH_EACH_REQUEST': True, 'SESSION_REFRESH_EACH_REQUEST': True,
'MAX_CONTENT_LENGTH': None, 'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12), 'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12),

11
flask/sessions.py

@ -249,6 +249,13 @@ class SessionInterface(object):
""" """
return app.config['SESSION_COOKIE_SECURE'] return app.config['SESSION_COOKIE_SECURE']
def get_cookie_samesite(self, app):
"""Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
``SameSite`` attribute. This currently just returns the value of
the :data:`SESSION_COOKIE_SAMESITE` setting.
"""
return app.config['SESSION_COOKIE_SAMESITE']
def get_expiration_time(self, app, session): def get_expiration_time(self, app, session):
"""A helper method that returns an expiration date for the session """A helper method that returns an expiration date for the session
or ``None`` if the session is linked to the browser session. The or ``None`` if the session is linked to the browser session. The
@ -362,6 +369,7 @@ class SecureCookieSessionInterface(SessionInterface):
httponly = self.get_cookie_httponly(app) httponly = self.get_cookie_httponly(app)
secure = self.get_cookie_secure(app) secure = self.get_cookie_secure(app)
samesite = self.get_cookie_samesite(app)
expires = self.get_expiration_time(app, session) expires = self.get_expiration_time(app, session)
val = self.get_signing_serializer(app).dumps(dict(session)) val = self.get_signing_serializer(app).dumps(dict(session))
response.set_cookie( response.set_cookie(
@ -371,5 +379,6 @@ class SecureCookieSessionInterface(SessionInterface):
httponly=httponly, httponly=httponly,
domain=domain, domain=domain,
path=path, path=path,
secure=secure secure=secure,
samesite=samesite
) )

29
tests/test_basic.py

@ -319,6 +319,7 @@ def test_session_using_session_settings(app, client):
SESSION_COOKIE_DOMAIN='.example.com', SESSION_COOKIE_DOMAIN='.example.com',
SESSION_COOKIE_HTTPONLY=False, SESSION_COOKIE_HTTPONLY=False,
SESSION_COOKIE_SECURE=True, SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_SAMESITE='Lax',
SESSION_COOKIE_PATH='/' SESSION_COOKIE_PATH='/'
) )
@ -333,6 +334,34 @@ def test_session_using_session_settings(app, client):
assert 'path=/' in cookie assert 'path=/' in cookie
assert 'secure' in cookie assert 'secure' in cookie
assert 'httponly' not in cookie assert 'httponly' not in cookie
assert 'samesite' in cookie
def test_session_using_samesite_attribute(app, client):
@app.route('/')
def index():
flask.session['testing'] = 42
return 'Hello World'
app.config.update(SESSION_COOKIE_SAMESITE='invalid')
with pytest.raises(ValueError):
client.get('/')
app.config.update(SESSION_COOKIE_SAMESITE=None)
rv = client.get('/')
cookie = rv.headers['set-cookie'].lower()
assert 'samesite' not in cookie
app.config.update(SESSION_COOKIE_SAMESITE='Strict')
rv = client.get('/')
cookie = rv.headers['set-cookie'].lower()
assert 'samesite=strict' in cookie
app.config.update(SESSION_COOKIE_SAMESITE='Lax')
rv = client.get('/')
cookie = rv.headers['set-cookie'].lower()
assert 'samesite=lax' in cookie
def test_session_localhost_warning(recwarn, app, client): def test_session_localhost_warning(recwarn, app, client):

2
tox.ini

@ -16,7 +16,7 @@ deps =
blinker blinker
python-dotenv python-dotenv
lowest: Werkzeug==0.9 lowest: Werkzeug==0.14
lowest: Jinja2==2.4 lowest: Jinja2==2.4
lowest: itsdangerous==0.21 lowest: itsdangerous==0.21
lowest: Click==4.0 lowest: Click==4.0

Loading…
Cancel
Save