From a1d9ebe4abebdb824aa9a397b001de5f9130e7a3 Mon Sep 17 00:00:00 2001 From: Fadhel_Chaabane Date: Tue, 23 Jan 2018 13:57:50 +0000 Subject: [PATCH 1/3] New Feature: Added Support for cookie's SameSite attribute. --- docs/config.rst | 9 ++++++++- docs/security.rst | 6 ++++-- flask/app.py | 1 + flask/sessions.py | 11 ++++++++++- tests/test_basic.py | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 777a1d28..c1854c8e 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -208,6 +208,14 @@ The following configuration values are used internally by Flask: Default: ``False`` +.. py:data:: SESSION_COOKIE_SAMESITE + + Browser will only send cookies to the domain that created them. + There are two possible values for the same-site attribute: "Strict" and "Lax" + If set to "None", the samesite flag is not set. + + Default: ``None`` + .. py:data:: PERMANENT_SESSION_LIFETIME If ``session.permanent`` is true, the cookie's expiration will be set this @@ -635,4 +643,3 @@ Example usage for both:: # or via open_instance_resource: with app.open_instance_resource('application.cfg') as f: config = f.read() - diff --git a/docs/security.rst b/docs/security.rst index 13ea2e33..b68e909e 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -195,16 +195,18 @@ They can be set on other cookies too. - ``HttpOnly`` protects the contents of cookies from being read with JavaScript. - ``SameSite`` ensures that cookies can only be requested from the same - domain that created them. It is not supported by Flask yet. + domain that created them. There are two possible values for the same-site + attribute: "Strict" and "Lax" :: app.config.update( SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE='Strict' ) - response.set_cookie('username', 'flask', secure=True, httponly=True) + response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Strict') Specifying ``Expires`` or ``Max-Age`` options, will remove the cookie after the given time, or the current time plus the age, respectively. If neither diff --git a/flask/app.py b/flask/app.py index a31eac91..200b5c20 100644 --- a/flask/app.py +++ b/flask/app.py @@ -284,6 +284,7 @@ class Flask(_PackageBoundObject): 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, + 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12), diff --git a/flask/sessions.py b/flask/sessions.py index 8f111718..eb028027 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -249,6 +249,13 @@ class SessionInterface(object): """ return app.config['SESSION_COOKIE_SECURE'] + def get_cookie_samesite(self, app): + """Returns "Strict", "Lax" or None if the cookie should use + samesite attribute. This currently just returns the value of + the ``SESSION_COOKIE_SAMESITE`` setting. + """ + return app.config['SESSION_COOKIE_SAMESITE'] + def get_expiration_time(self, app, session): """A helper method that returns an expiration date for the session 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) secure = self.get_cookie_secure(app) + samesite = self.get_cookie_samesite(app) expires = self.get_expiration_time(app, session) val = self.get_signing_serializer(app).dumps(dict(session)) response.set_cookie( @@ -371,5 +379,6 @@ class SecureCookieSessionInterface(SessionInterface): httponly=httponly, domain=domain, path=path, - secure=secure + secure=secure, + samesite=samesite ) diff --git a/tests/test_basic.py b/tests/test_basic.py index d7406baf..b0397ee6 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -319,6 +319,7 @@ def test_session_using_session_settings(app, client): SESSION_COOKIE_DOMAIN='.example.com', SESSION_COOKIE_HTTPONLY=False, SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_SAMESITE='Strict', SESSION_COOKIE_PATH='/' ) @@ -333,8 +334,45 @@ def test_session_using_session_settings(app, client): assert 'path=/' in cookie assert 'secure' in cookie assert 'httponly' not in cookie + assert 'samesite' in cookie +def test_session_using_samesite_attribute(app, client): + app.config.update( + SERVER_NAME='www.example.com:8080', + APPLICATION_ROOT='/test', + SESSION_COOKIE_DOMAIN='.example.com', + SESSION_COOKIE_HTTPONLY=False, + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_SAMESITE='anyvalue', + SESSION_COOKIE_PATH='/' + ) + + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'Hello World' + + # assert excption when samesite is not set to 'Strict', 'Lax' or None + with pytest.raises(ValueError): + rv = client.get('/', 'http://www.example.com:8080/test/') + + # assert the samesite flag is not set in the cookie, when set to None + app.config.update(SESSION_COOKIE_SAMESITE=None) + rv = client.get('/', 'http://www.example.com:8080/test/') + cookie = rv.headers['set-cookie'].lower() + assert 'samesite' not in cookie + + app.config.update(SESSION_COOKIE_SAMESITE='Strict') + rv = client.get('/', 'http://www.example.com:8080/test/') + cookie = rv.headers['set-cookie'].lower() + assert 'samesite=strict' in cookie + + app.config.update(SESSION_COOKIE_SAMESITE='Lax') + rv = client.get('/', 'http://www.example.com:8080/test/') + cookie = rv.headers['set-cookie'].lower() + assert 'samesite=lax' in cookie + def test_session_localhost_warning(recwarn, app, client): app.config.update( SERVER_NAME='localhost:5000', From db5735c3ceaa04e9d9d05d942686bd6e369d0f34 Mon Sep 17 00:00:00 2001 From: Fadhel_Chaabane Date: Tue, 23 Jan 2018 15:02:07 +0000 Subject: [PATCH 2/3] Changed Werkzeug min version to 0.14 to support SameSite cookie's attribute --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4bfaaed1..a88f5fff 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ deps = blinker python-dotenv - lowest: Werkzeug==0.9 + lowest: Werkzeug==0.14 lowest: Jinja2==2.4 lowest: itsdangerous==0.21 lowest: Click==4.0 From 382b13581ed44e0bc740968353bb5a25945bafdf Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 23 Jan 2018 15:11:50 -0800 Subject: [PATCH 3/3] clean up samesite docs --- docs/config.rst | 12 ++++++++---- docs/security.rst | 18 +++++++++++++----- flask/sessions.py | 6 +++--- tests/test_basic.py | 25 ++++++++----------------- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index c1854c8e..2e2833f9 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -210,12 +210,14 @@ The following configuration values are used internally by Flask: .. py:data:: SESSION_COOKIE_SAMESITE - Browser will only send cookies to the domain that created them. - There are two possible values for the same-site attribute: "Strict" and "Lax" - If set to "None", the samesite flag is not set. + 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 If ``session.permanent`` is true, the cookie's expiration will be set this @@ -369,13 +371,15 @@ The following configuration values are used internally by Flask: ``LOGGER_HANDLER_POLICY``, ``EXPLAIN_TEMPLATE_LOADING`` .. versionchanged:: 1.0 - ``LOGGER_NAME`` and ``LOGGER_HANDLER_POLICY`` were removed. See :ref:`logging` for information about configuration. Added :data:`ENV` to reflect the :envvar:`FLASK_ENV` environment variable. + Added :data:`SESSION_COOKIE_SAMESITE` to control the session + cookie's ``SameSite`` option. + Configuring from Files ---------------------- diff --git a/docs/security.rst b/docs/security.rst index b68e909e..44c095ac 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -184,6 +184,9 @@ contains the same data. :: - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection + +.. _security-cookie: + Set-Cookie options ~~~~~~~~~~~~~~~~~~ @@ -194,19 +197,21 @@ They can be set on other cookies too. - ``Secure`` limits cookies to HTTPS traffic only. - ``HttpOnly`` protects the contents of cookies from being read with JavaScript. -- ``SameSite`` ensures that cookies can only be requested from the same - domain that created them. There are two possible values for the same-site - attribute: "Strict" and "Lax" +- ``SameSite`` restricts how cookies are sent with requests from + 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( SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True, - SESSION_COOKIE_SAMESITE='Strict' + SESSION_COOKIE_SAMESITE='Lax', ) - response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Strict') + response.set_cookie('username', 'flask', secure=True, httponly=True, samesite='Lax') Specifying ``Expires`` or ``Max-Age`` options, will remove the cookie after the given time, or the current time plus the age, respectively. If neither @@ -239,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/Headers/Set-Cookie +.. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute + + HTTP Public Key Pinning (HPKP) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/flask/sessions.py b/flask/sessions.py index eb028027..621f3f5e 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -250,9 +250,9 @@ class SessionInterface(object): return app.config['SESSION_COOKIE_SECURE'] def get_cookie_samesite(self, app): - """Returns "Strict", "Lax" or None if the cookie should use - samesite attribute. This currently just returns the value of - the ``SESSION_COOKIE_SAMESITE`` setting. + """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'] diff --git a/tests/test_basic.py b/tests/test_basic.py index b0397ee6..0e55b52e 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -319,7 +319,7 @@ def test_session_using_session_settings(app, client): SESSION_COOKIE_DOMAIN='.example.com', SESSION_COOKIE_HTTPONLY=False, SESSION_COOKIE_SECURE=True, - SESSION_COOKIE_SAMESITE='Strict', + SESSION_COOKIE_SAMESITE='Lax', SESSION_COOKIE_PATH='/' ) @@ -338,41 +338,32 @@ def test_session_using_session_settings(app, client): def test_session_using_samesite_attribute(app, client): - app.config.update( - SERVER_NAME='www.example.com:8080', - APPLICATION_ROOT='/test', - SESSION_COOKIE_DOMAIN='.example.com', - SESSION_COOKIE_HTTPONLY=False, - SESSION_COOKIE_SECURE=True, - SESSION_COOKIE_SAMESITE='anyvalue', - SESSION_COOKIE_PATH='/' - ) - @app.route('/') def index(): flask.session['testing'] = 42 return 'Hello World' - # assert excption when samesite is not set to 'Strict', 'Lax' or None + app.config.update(SESSION_COOKIE_SAMESITE='invalid') + with pytest.raises(ValueError): - rv = client.get('/', 'http://www.example.com:8080/test/') + client.get('/') - # assert the samesite flag is not set in the cookie, when set to None app.config.update(SESSION_COOKIE_SAMESITE=None) - rv = client.get('/', 'http://www.example.com:8080/test/') + rv = client.get('/') cookie = rv.headers['set-cookie'].lower() assert 'samesite' not in cookie app.config.update(SESSION_COOKIE_SAMESITE='Strict') - rv = client.get('/', 'http://www.example.com:8080/test/') + rv = client.get('/') cookie = rv.headers['set-cookie'].lower() assert 'samesite=strict' in cookie app.config.update(SESSION_COOKIE_SAMESITE='Lax') - rv = client.get('/', 'http://www.example.com:8080/test/') + rv = client.get('/') cookie = rv.headers['set-cookie'].lower() assert 'samesite=lax' in cookie + def test_session_localhost_warning(recwarn, app, client): app.config.update( SERVER_NAME='localhost:5000',