diff --git a/README.md b/README.md index 90c4b6f4..48c87bdc 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,17 @@ Professional support for Rdiffweb is available by contacting [IKUS Soft](https:/ # Changelog +## 2.4.2 (2022-09-08) + +This releases include a security fix. If you are using an earlier version, you should upgrade to this release immediately. + +* Use 'Secure' Attribute with Sensitive Cookie in HTTPS Session. [CVE-2022-3174](https://nvd.nist.gov/vuln/detail/CVE-2022-3174) #209 + ## 2.4.1 (2022-09-08) -* Add Clickjacking Defense +This releases include a security fix. If you are using an earlier version, you should upgrade to this release immediately. + +* Add Clickjacking Defense [CVE-2022-3167](https://nvd.nist.gov/vuln/detail/CVE-2022-3167) * Drop Ubuntu Hirsute & Impish (End-of-life) ## 2.4.0 (2022-06-21) diff --git a/rdiffweb/controller/tests/test_csrf.py b/rdiffweb/controller/tests/test_secure_headers.py similarity index 79% rename from rdiffweb/controller/tests/test_csrf.py rename to rdiffweb/controller/tests/test_secure_headers.py index af58d0c2..58063fe3 100644 --- a/rdiffweb/controller/tests/test_csrf.py +++ b/rdiffweb/controller/tests/test_secure_headers.py @@ -22,11 +22,11 @@ import rdiffweb.test -class CsrfTest(rdiffweb.test.WebCase): +class SecureHeadersTest(rdiffweb.test.WebCase): login = True - def test_samesite_lax(self): + def test_cookie_samesite_lax(self): # Given a request made to rdiffweb # When receiving the response self.getPage('/') @@ -34,7 +34,7 @@ def test_samesite_lax(self): cookie = self.assertHeader('Set-Cookie') self.assertIn('SameSite=Lax', cookie) - def test_samesite_lax_without_session(self): + def test_cookie_samesite_lax_without_session(self): # Given not a client sending no cookie self.cookies = None # When a query is made to a static path (without session) @@ -42,6 +42,22 @@ def test_samesite_lax_without_session(self): # Then Set-Cookie is not defined. self.assertNoHeader('Set-Cookie') + def test_cookie_with_https(self): + # Given an https request made to rdiffweb + self.getPage('/', headers=[('X-Forwarded-Proto', 'https')]) + # When receiving the response + # Then the header contains Set-Cookie with Secure + cookie = self.assertHeader('Set-Cookie') + self.assertIn('Secure', cookie) + + def test_cookie_with_http(self): + # Given an https request made to rdiffweb + self.getPage('/') + # When receiving the response + # Then the header contains Set-Cookie with Secure + cookie = self.assertHeader('Set-Cookie') + self.assertNotIn('Secure', cookie) + def test_get_with_wrong_origin(self): # Given a GET request made to rdiffweb # When the request is made using a different origin diff --git a/rdiffweb/rdw_app.py b/rdiffweb/rdw_app.py index f2ccacc8..317b90ff 100644 --- a/rdiffweb/rdw_app.py +++ b/rdiffweb/rdw_app.py @@ -38,7 +38,7 @@ import rdiffweb.tools.i18n import rdiffweb.tools.proxy import rdiffweb.tools.ratelimit -import rdiffweb.tools.security +import rdiffweb.tools.secure_headers from rdiffweb.controller import Controller from rdiffweb.controller.api import ApiPage from rdiffweb.controller.dispatch import static # noqa @@ -63,6 +63,7 @@ @cherrypy.tools.proxy() +@cherrypy.tools.secure_headers() class Root(LocationsPage): def __init__(self): self.login = LoginPage() @@ -170,7 +171,6 @@ def __init__(self, cfg): 'tools.auth_form.on': True, 'tools.currentuser.on': True, 'tools.currentuser.userobj': lambda username: self.store.get_user(username), - 'tools.csrf.on': True, 'tools.i18n.on': True, 'tools.i18n.default': 'en_US', 'tools.i18n.mo_dir': pkg_resources.resource_filename('rdiffweb', 'locales'), # @UndefinedVariable diff --git a/rdiffweb/tools/security.py b/rdiffweb/tools/secure_headers.py similarity index 81% rename from rdiffweb/tools/security.py rename to rdiffweb/tools/secure_headers.py index 6fe62659..202fc26b 100644 --- a/rdiffweb/tools/security.py +++ b/rdiffweb/tools/secure_headers.py @@ -32,12 +32,13 @@ http.cookies.Morsel._reserved['samesite'] = 'SameSite' -class CsrfAuth(HandlerTool): +class SecureHeaders(HandlerTool): """ This tool provide CSRF mitigation. * Define X-Frame-Options = DENY * Define Cookies SameSite=Lax + * Define Cookies Secure when https is detected * Validate `Origin` and `Referer` on POST, PUT, PATCH, DELETE Ref.: @@ -46,7 +47,7 @@ class CsrfAuth(HandlerTool): """ def __init__(self): - HandlerTool.__init__(self, self.run, name='csrf') + HandlerTool.__init__(self, self.run, name='secure_headers') # Make sure to run before authform (priority 71) self._priority = 71 @@ -58,12 +59,18 @@ def _set_headers(self): response = cherrypy.serving.response # Define X-Frame-Options to avoid Clickjacking response.headers['X-Frame-Options'] = 'DENY' - # Awaiting bug fix in cherrypy - # https://github.com/cherrypy/cherrypy/issues/1767 - # Force SameSite to Lax + + # Enforce security on cookies cookie = response.cookie.get('session_id', None) if cookie: + # Awaiting bug fix in cherrypy + # https://github.com/cherrypy/cherrypy/issues/1767 + # Force SameSite to Lax cookie['samesite'] = 'Lax' + # Check if https is enabled + https = cherrypy.request.base.startswith('https') + if https: + cookie['secure'] = 1 def run(self): if cherrypy.request.method in ['POST', 'PUT', 'PATCH', 'DELETE']: @@ -73,4 +80,4 @@ def run(self): raise cherrypy.HTTPError(403, 'Unexpected Origin header') -cherrypy.tools.csrf = CsrfAuth() +cherrypy.tools.secure_headers = SecureHeaders()