[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 `/<module>/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) <juc@odoo.com>
This commit is contained in:
Julien Castiaux
2023-12-18 23:32:01 +00:00
parent 59564a4f4a
commit 3f8296a16e
6 changed files with 42 additions and 1 deletions
+8
View File
@@ -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')
+8
View File
@@ -16,6 +16,14 @@
<field name="public">True</field>
</record>
<record id="xss_svg" model="ir.attachment">
<field name="name">xss.svg</field>
<field name="type">binary</field>
<field name="datas" type="base64" file="test_http/static/src/img/xss.svg"/>
<field name="url">/test_http/static/src/img/xss.svg</field>
<field name="public">True</field>
</record>
<record id="rickroll" model="ir.attachment">
<field name="name">rickroll</field>
<field name="type">url</field>
@@ -0,0 +1,3 @@
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<script type="text/javascript" href="/test_http/fail"></script>
</svg>

After

Width:  |  Height:  |  Size: 146 B

+1
View File
@@ -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
+19
View File
@@ -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)
+3 -1
View File
@@ -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