From 3f8296a16eecc3554441b616ac2e037df74661f3 Mon Sep 17 00:00:00 2001 From: Julien Castiaux Date: Fri, 11 Aug 2023 13:01:12 +0200 Subject: [PATCH] [FIX] core: set Content-Security-Policy on static The Content-Security-Policy[^1] http header was only set on the response generated by controllers but it was missing from the `//static/` route. It is not strictly necessary to set that header on the responses comming from that routes as it is not possible to add new static files or edit existing ones via the interface (not even as admin). Only the developers and system administrator can access those files. It is also worth mentionning that using the Odoo internal web server to deliver static files is suboptimal. Outside of a dev environment, those files will typically be delivered via a web server[^2] and sysadmins should configure their web server to set the CSP header on static images. [^1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP [^2]: https://www.odoo.com/documentation/master/administration/install/deploy.html#serving-static-files-and-attachments closes odoo/odoo#146591 X-original-commit: 55e09d504df9bd134afb6e0b38f03457b5c71e8e Related: odoo/documentation#6953 Signed-off-by: Julien Castiaux (juc) --- odoo/addons/test_http/controllers.py | 8 ++++++++ odoo/addons/test_http/data.xml | 8 ++++++++ odoo/addons/test_http/static/src/img/xss.svg | 3 +++ odoo/addons/test_http/tests/__init__.py | 1 + odoo/addons/test_http/tests/test_xss.py | 19 +++++++++++++++++++ odoo/http.py | 4 +++- 6 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 odoo/addons/test_http/static/src/img/xss.svg create mode 100644 odoo/addons/test_http/tests/test_xss.py diff --git a/odoo/addons/test_http/controllers.py b/odoo/addons/test_http/controllers.py index dba5f5299e1..4da4da7ea73 100644 --- a/odoo/addons/test_http/controllers.py +++ b/odoo/addons/test_http/controllers.py @@ -170,6 +170,14 @@ class TestHttp(http.Controller): # ===================================================== # Errors # ===================================================== + @http.route('/test_http/fail', type='http', auth='none') + def fail(self): + _logger.error( + "The /test_http/fail route should never be called, referrer: %s", + http.request.httprequest.headers.get('referer') + ) + raise request.not_found() + @http.route('/test_http/json_value_error', type='json', auth='none') def json_value_error(self): raise ValueError('Unknown destination') diff --git a/odoo/addons/test_http/data.xml b/odoo/addons/test_http/data.xml index 023555bc903..3bb2d39197a 100644 --- a/odoo/addons/test_http/data.xml +++ b/odoo/addons/test_http/data.xml @@ -16,6 +16,14 @@ True + + xss.svg + binary + + /test_http/static/src/img/xss.svg + True + + rickroll url diff --git a/odoo/addons/test_http/static/src/img/xss.svg b/odoo/addons/test_http/static/src/img/xss.svg new file mode 100644 index 00000000000..37519ab0781 --- /dev/null +++ b/odoo/addons/test_http/static/src/img/xss.svg @@ -0,0 +1,3 @@ + + + diff --git a/odoo/addons/test_http/tests/__init__.py b/odoo/addons/test_http/tests/__init__.py index ad63b65fcac..cc771f36c03 100644 --- a/odoo/addons/test_http/tests/__init__.py +++ b/odoo/addons/test_http/tests/__init__.py @@ -7,3 +7,4 @@ from . import test_models from . import test_session from . import test_static from . import test_web_server +from . import test_xss diff --git a/odoo/addons/test_http/tests/test_xss.py b/odoo/addons/test_http/tests/test_xss.py new file mode 100644 index 00000000000..f8fc3a9a62b --- /dev/null +++ b/odoo/addons/test_http/tests/test_xss.py @@ -0,0 +1,19 @@ +from odoo.tests import tagged +from .test_common import TestHttpBase + + +@tagged('-at_install', 'post_install') +class TestXSS(TestHttpBase): + # In case a XSS isn't filtered, it'll load /test_http/fail which logs an error. + # browser_js only returns when it finds "test successful" in the logs. + fake_success = "console.log('test successfulness cannot be determined via JS')" + # ^^^^^^^^^^^^^^^ + + def test_xss_static(self): + self.browser_js('/test_http/static/src/img/xss.svg', self.fake_success) + + def test_xss_web_content(self): + self.browser_js('/web/content/test_http.xss_svg', self.fake_success) + + def test_xss_web_image(self): + self.browser_js('/web/image/test_http.xss_svg', self.fake_success) diff --git a/odoo/http.py b/odoo/http.py index 8e6fe8f1cb0..fe742ba5553 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -1681,9 +1681,11 @@ class Request: try: directory = root.statics[module] filepath = werkzeug.security.safe_join(directory, path) - return Stream.from_path(filepath).get_response( + res = Stream.from_path(filepath).get_response( max_age=0 if 'assets' in self.session.debug else STATIC_CACHE, ) + root.set_csp(res) + return res except KeyError: raise NotFound(f'Module "{module}" not found.\n') except OSError: # cover both missing file and invalid permissions