diff --git a/docs/topics/development/troubleshooting_and_debugging.md b/docs/topics/development/troubleshooting_and_debugging.md index 1ad150e06f20..4cfa33996fe2 100644 --- a/docs/topics/development/troubleshooting_and_debugging.md +++ b/docs/topics/development/troubleshooting_and_debugging.md @@ -91,7 +91,7 @@ To use it, see the official getting started docs: [Django Debug Toolbar Installa - The Django Debug Toolbar can slow down the website. Mitigate this by deselecting the checkbox next to the `SQL` panel. - Use the Django Debug Toolbar only when needed, as it affects CSP report only for your local dev environment. -- You might need to disable CSP by setting `CSP_REPORT_ONLY = True` in your local settings because the Django Debug Toolbar uses "data:" for its logo and "unsafe eval" for some panels like templates or SQL. +- You might need to disable CSP by setting `CONTENT_SECURITY_POLICY_REPORT_ONLY = CONTENT_SECURITY_POLICY_REPORT;CONTENT_SECURITY_POLICY_REPORT = {}` in your local settings because the Django Debug Toolbar uses "data:" for its logo and "unsafe eval" for some panels like templates or SQL. ## Additional Debugging Tools diff --git a/requirements/prod.txt b/requirements/prod.txt index bfd738ac0e36..5658bc2b53cd 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -581,9 +581,9 @@ django-admin-rangefilter==0.13.3 \ django-cors-headers==4.7.0 \ --hash=sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b \ --hash=sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070 -django_csp==3.8 \ - --hash=sha256:19b2978b03fcd73517d7d67acbc04fbbcaec0facc3e83baa502965892d1e0719 \ - --hash=sha256:ef0f1a9f7d8da68ae6e169c02e9ac661c0ecf04db70e0d1d85640512a68471c0 +django_csp==4.0 \ + --hash=sha256:b27010bb702eb20a3dad329178df2b61a2b82d338b70fbdc13c3a3bd28712833 \ + --hash=sha256:d5a0a05463a6b75a4f1fc1828c58c89af8db9364d09fc6e12f122b4d7f3d00dc django-environ==0.11.2 \ --hash=sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05 \ --hash=sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be diff --git a/settings.py b/settings.py index c913a56e3f03..cbf0d752bc25 100644 --- a/settings.py +++ b/settings.py @@ -7,6 +7,7 @@ """ import os +from copy import deepcopy from urllib.parse import urlparse from olympia.core.utils import get_version_json @@ -117,19 +118,19 @@ def insert_debug_toolbar_middleware(middlewares): FXA_OAUTH_HOST = 'https://oauth.stage.mozaws.net/v1' FXA_PROFILE_HOST = 'https://profile.stage.mozaws.net/v1' -# CSP report endpoint which returns a 204 from addons-nginx in local dev. -CSP_REPORT_URI = '/csp-report' -RESTRICTED_DOWNLOAD_CSP['REPORT_URI'] = CSP_REPORT_URI - # Set CSP like we do for dev/stage/prod, but also allow GA over http + www subdomain # for local development. HTTP_GA_SRC = 'http://www.google-analytics.com' -CSP_CONNECT_SRC += (SITE_URL,) -CSP_FONT_SRC += (STATIC_URL,) -CSP_IMG_SRC += (MEDIA_URL, STATIC_URL, HTTP_GA_SRC) -CSP_SCRIPT_SRC += (STATIC_URL, HTTP_GA_SRC) -CSP_STYLE_SRC += (STATIC_URL,) +# we want to be able to test the settings_base without these overrides interfering +CONTENT_SECURITY_POLICY = deepcopy(CONTENT_SECURITY_POLICY) +CONTENT_SECURITY_POLICY['DIRECTIVES']['connect-src'] += (SITE_URL,) +CONTENT_SECURITY_POLICY['DIRECTIVES']['font-src'] += (STATIC_URL,) +CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] += (MEDIA_URL, STATIC_URL, HTTP_GA_SRC) +CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] += (STATIC_URL, HTTP_GA_SRC) +CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src'] += (STATIC_URL,) +# CSP report endpoint which returns a 204 from addons-nginx in local dev. +CONTENT_SECURITY_POLICY['DIRECTIVES']['report-uri'] = '/csp-report' # Auth token required to authorize inbound email. INBOUND_EMAIL_SECRET_KEY = 'totally-unsecure-secret-string' diff --git a/src/olympia/amo/tests/test_csp_headers.py b/src/olympia/amo/tests/test_csp_headers.py index efc7e25a4e7d..021a0504cc75 100644 --- a/src/olympia/amo/tests/test_csp_headers.py +++ b/src/olympia/amo/tests/test_csp_headers.py @@ -1,17 +1,15 @@ import os from django.conf import settings -from django.test.utils import override_settings from olympia.amo.tests import TestCase from olympia.lib import settings_base as base_settings def test_default_settings_no_report_only(): - assert settings.CSP_REPORT_ONLY is False + assert getattr(settings, 'CONTENT_SECURITY_POLICY', {}).keys() -@override_settings(CSP_REPORT_ONLY=False) class TestCSPHeaders(TestCase): def test_for_specific_csp_settings(self): """Test that required settings are provided as headers.""" @@ -35,101 +33,191 @@ def test_for_specific_csp_settings(self): def test_unsafe_inline_not_in_script_src(self): """Make sure a script-src does not have unsafe-inline.""" - assert "'unsafe-inline'" not in base_settings.CSP_SCRIPT_SRC + assert ( + "'unsafe-inline'" + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) def test_unsafe_eval_not_in_script_src(self): """Make sure a script-src does not have unsafe-eval.""" - assert "'unsafe-eval'" not in base_settings.CSP_SCRIPT_SRC + assert ( + "'unsafe-eval'" + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) def test_data_and_blob_not_in_script_and_style_src(self): """Make sure a script-src/style-src does not have data: or blob:.""" - assert 'blob:' not in base_settings.CSP_SCRIPT_SRC - assert 'data:' not in base_settings.CSP_SCRIPT_SRC - assert 'blob:' not in base_settings.CSP_STYLE_SRC - assert 'data:' not in base_settings.CSP_STYLE_SRC + assert ( + 'blob:' + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) + assert ( + 'data:' + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) + assert ( + 'blob:' + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src'] + ) + assert ( + 'data:' + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src'] + ) def test_http_protocol_not_in_script_src(self): """Make sure a script-src does not have hosts using http:.""" - for val in base_settings.CSP_SCRIPT_SRC: + for val in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src']: assert not val.startswith('http:') def test_http_protocol_not_in_frame_src(self): """Make sure a frame-src does not have hosts using http:.""" - for val in base_settings.CSP_FRAME_SRC: + for val in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['frame-src']: assert not val.startswith('http:') def test_http_protocol_not_in_child_src(self): """Make sure a child-src does not have hosts using http:.""" - for val in base_settings.CSP_CHILD_SRC: + for val in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['child-src']: assert not val.startswith('http:') def test_http_protocol_not_in_style_src(self): """Make sure a style-src does not have hosts using http:.""" - for val in base_settings.CSP_STYLE_SRC: + for val in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src']: assert not val.startswith('http:') def test_http_protocol_not_in_img_src(self): """Make sure a img-src does not have hosts using http:.""" - for val in base_settings.CSP_IMG_SRC: + for val in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src']: assert not val.startswith('http:') def test_blob_and_data_in_img_src(self): """Test that img-src contains data/blob.""" - assert 'blob:' in base_settings.CSP_IMG_SRC - assert 'data:' in base_settings.CSP_IMG_SRC + assert 'blob:' in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] + assert 'data:' in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] def test_child_src_matches_frame_src(self): """Check frame-src directive has same settings as child-src""" - assert base_settings.CSP_FRAME_SRC == base_settings.CSP_CHILD_SRC + assert ( + base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['frame-src'] + == base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['child-src'] + ) def test_prod_static_url_in_common_settings(self): """Make sure prod cdn is specified by default for statics.""" prod_static_url = base_settings.PROD_STATIC_URL - assert prod_static_url in base_settings.CSP_FONT_SRC - assert prod_static_url in base_settings.CSP_IMG_SRC - assert prod_static_url in base_settings.CSP_SCRIPT_SRC - assert prod_static_url in base_settings.CSP_STYLE_SRC + assert ( + prod_static_url + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['font-src'] + ) + assert ( + prod_static_url + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] + ) + assert ( + prod_static_url + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) + assert ( + prod_static_url + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src'] + ) prod_media_url = base_settings.PROD_MEDIA_URL - assert prod_media_url not in base_settings.CSP_FONT_SRC - assert prod_media_url in base_settings.CSP_IMG_SRC - assert prod_media_url not in base_settings.CSP_SCRIPT_SRC - assert prod_media_url not in base_settings.CSP_STYLE_SRC + assert ( + prod_media_url + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['font-src'] + ) + assert ( + prod_media_url + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] + ) + assert ( + prod_media_url + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) + assert ( + prod_media_url + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src'] + ) def test_self_in_common_settings(self): """Check 'self' is defined for common settings.""" - assert "'self'" in base_settings.CSP_CONNECT_SRC - assert "'self'" in base_settings.CSP_IMG_SRC - assert "'self'" in base_settings.CSP_FORM_ACTION + assert ( + "'self'" + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['connect-src'] + ) + assert ( + "'self'" in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] + ) + assert ( + "'self'" + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['form-action'] + ) def test_not_self_in_script_child_or_style_src(self): """script-src/style-src/child-src should not need 'self' or the entire a.m.o. domain""" - assert "'self'" not in base_settings.CSP_SCRIPT_SRC - assert 'https://addons.mozilla.org' not in base_settings.CSP_SCRIPT_SRC - assert "'self'" not in base_settings.CSP_STYLE_SRC - assert 'https://addons.mozilla.org' not in base_settings.CSP_STYLE_SRC - assert "'self'" not in base_settings.CSP_CHILD_SRC - assert 'https://addons.mozilla.org' not in base_settings.CSP_CHILD_SRC + assert ( + "'self'" + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) + assert ( + 'https://addons.mozilla.org' + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) + assert ( + "'self'" + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src'] + ) + assert ( + 'https://addons.mozilla.org' + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src'] + ) + assert ( + "'self'" + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['child-src'] + ) + assert ( + 'https://addons.mozilla.org' + not in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['child-src'] + ) def test_analytics_in_common_settings(self): """Check for anaytics hosts in connect-src, img-src and script-src""" # See https://github.com/mozilla/addons/issues/14799#issuecomment-2127359422 - assert base_settings.GOOGLE_ANALYTICS_HOST in base_settings.CSP_CONNECT_SRC - assert base_settings.GOOGLE_TAGMANAGER_HOST in base_settings.CSP_CONNECT_SRC + assert ( + base_settings.GOOGLE_ANALYTICS_HOST + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['connect-src'] + ) + assert ( + base_settings.GOOGLE_TAGMANAGER_HOST + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['connect-src'] + ) assert ( base_settings.GOOGLE_ADDITIONAL_ANALYTICS_HOST - in base_settings.CSP_CONNECT_SRC + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['connect-src'] ) - assert base_settings.GOOGLE_ANALYTICS_HOST in base_settings.CSP_IMG_SRC - assert base_settings.GOOGLE_TAGMANAGER_HOST in base_settings.CSP_IMG_SRC + assert ( + base_settings.GOOGLE_ANALYTICS_HOST + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] + ) + assert ( + base_settings.GOOGLE_TAGMANAGER_HOST + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] + ) - assert base_settings.GOOGLE_ANALYTICS_HOST in base_settings.CSP_SCRIPT_SRC - assert base_settings.GOOGLE_TAGMANAGER_HOST in base_settings.CSP_SCRIPT_SRC + assert ( + base_settings.GOOGLE_ANALYTICS_HOST + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) + assert ( + base_settings.GOOGLE_TAGMANAGER_HOST + in base_settings.CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] + ) def test_csp_settings_not_overriden_for_prod(self): - """Checks sites/prod/settings.py doesn't have CSP_* settings. + """Checks sites/prod/settings.py doesn't change CONTENT_SECURITY_POLICY + settings. Because testing the import of site settings is difficult due to env vars, we specify prod settings in lib/base_settings and then @@ -145,4 +233,4 @@ def test_csp_settings_not_overriden_for_prod(self): with open(path) as f: data = f.read() - assert 'CSP_' not in data + assert 'CONTENT_SECURITY_POLICY' not in data diff --git a/src/olympia/conf/dev/settings.py b/src/olympia/conf/dev/settings.py index 929b6a791185..cc4f3155ee8a 100644 --- a/src/olympia/conf/dev/settings.py +++ b/src/olympia/conf/dev/settings.py @@ -25,12 +25,12 @@ STATIC_URL = '%s/static-server/' % EXTERNAL_SITE_URL MEDIA_URL = '%s/user-media/' % EXTERNAL_SITE_URL -CSP_FONT_SRC += (STATIC_URL,) -# CSP_IMG_SRC already contains 'self', but we could be on reviewers or admin +CONTENT_SECURITY_POLICY['DIRECTIVES']['font-src'] += (STATIC_URL,) +# img-src already contains 'self', but we could be on reviewers or admin # domain and want to load things from the regular domain. -CSP_IMG_SRC += (MEDIA_URL, STATIC_URL) -CSP_SCRIPT_SRC += (STATIC_URL,) -CSP_STYLE_SRC += (STATIC_URL,) +CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] += (MEDIA_URL, STATIC_URL) +CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] += (STATIC_URL,) +CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src'] += (STATIC_URL,) SESSION_COOKIE_DOMAIN = '.%s' % DOMAIN diff --git a/src/olympia/conf/stage/settings.py b/src/olympia/conf/stage/settings.py index 3c960f0c8931..beed9235a7ce 100644 --- a/src/olympia/conf/stage/settings.py +++ b/src/olympia/conf/stage/settings.py @@ -24,12 +24,12 @@ STATIC_URL = '%s/static-server/' % EXTERNAL_SITE_URL MEDIA_URL = '%s/user-media/' % EXTERNAL_SITE_URL -CSP_FONT_SRC += (STATIC_URL,) -# CSP_IMG_SRC already contains 'self', but we could be on reviewers or admin +CONTENT_SECURITY_POLICY['DIRECTIVES']['font-src'] += (STATIC_URL,) +# img-src already contains 'self', but we could be on reviewers or admin # domain and want to load things from the regular domain. -CSP_IMG_SRC += (MEDIA_URL, STATIC_URL) -CSP_SCRIPT_SRC += (STATIC_URL,) -CSP_STYLE_SRC += (STATIC_URL,) +CONTENT_SECURITY_POLICY['DIRECTIVES']['img-src'] += (MEDIA_URL, STATIC_URL) +CONTENT_SECURITY_POLICY['DIRECTIVES']['script-src'] += (STATIC_URL,) +CONTENT_SECURITY_POLICY['DIRECTIVES']['style-src'] += (STATIC_URL,) SESSION_COOKIE_DOMAIN = '.%s' % DOMAIN diff --git a/src/olympia/devhub/views.py b/src/olympia/devhub/views.py index 33baf0713ad4..c2723766fe34 100644 --- a/src/olympia/devhub/views.py +++ b/src/olympia/devhub/views.py @@ -144,8 +144,10 @@ def addon_listing(request, theme=False): @csp_update( - CONNECT_SRC=settings.MOZILLA_NEWLETTER_URL, - FORM_ACTION=settings.MOZILLA_NEWLETTER_URL, + { + 'connect-src': [settings.MOZILLA_NEWLETTER_URL], + 'form-action': [settings.MOZILLA_NEWLETTER_URL], + } ) def index(request): ctx = {} diff --git a/src/olympia/lib/settings_base.py b/src/olympia/lib/settings_base.py index 16767d0942a1..583c686c27a0 100644 --- a/src/olympia/lib/settings_base.py +++ b/src/olympia/lib/settings_base.py @@ -968,59 +968,46 @@ def get_language_url_map(): GOOGLE_ANALYTICS_HOST = 'https://*.google-analytics.com' GOOGLE_ADDITIONAL_ANALYTICS_HOST = 'https://*.analytics.google.com' - -CSP_REPORT_URI = '/__cspreport__' -CSP_REPORT_ONLY = False -CSP_EXCLUDE_URL_PREFIXES = () - -# NOTE: CSP_DEFAULT_SRC MUST be set otherwise things not set -# will default to being open to anything. -CSP_DEFAULT_SRC = ("'none'",) -CSP_CONNECT_SRC = ( - "'self'", - GOOGLE_ANALYTICS_HOST, - GOOGLE_ADDITIONAL_ANALYTICS_HOST, - GOOGLE_TAGMANAGER_HOST, -) -CSP_FORM_ACTION = ("'self'",) -CSP_FONT_SRC = ( - "'self'", - PROD_STATIC_URL, -) -CSP_CHILD_SRC = ('https://www.recaptcha.net/recaptcha/',) -CSP_FRAME_SRC = CSP_CHILD_SRC -CSP_IMG_SRC = ( - "'self'", - 'blob:', # Needed for image uploads. - 'data:', # Needed for theme wizard. - PROD_STATIC_URL, - PROD_MEDIA_URL, - GOOGLE_ANALYTICS_HOST, - GOOGLE_TAGMANAGER_HOST, -) -CSP_MEDIA_SRC = ('https://videos.cdn.mozilla.net',) -CSP_OBJECT_SRC = ("'none'",) - -CSP_SCRIPT_SRC = ( - GOOGLE_ANALYTICS_HOST, - GOOGLE_TAGMANAGER_HOST, - 'https://www.recaptcha.net/recaptcha/', - 'https://www.gstatic.com/recaptcha/', - 'https://www.gstatic.cn/recaptcha/', - PROD_STATIC_URL, -) -CSP_STYLE_SRC = ( - "'unsafe-inline'", - PROD_STATIC_URL, -) - -RESTRICTED_DOWNLOAD_CSP = { - 'DEFAULT_SRC': "'none'", - 'BASE_URI': "'none'", - 'FORM_ACTION': "'none'", - 'OBJECT_SRC': "'none'", - 'FRAME_ANCESTORS': "'none'", - 'REPORT_URI': CSP_REPORT_URI, +RECAPTCHA_URL = 'https://www.recaptcha.net/recaptcha/' + +CONTENT_SECURITY_POLICY = { + 'DIRECTIVES': { + # NOTE: default-src MUST be set otherwise things not set + # will default to being open to anything. + 'default-src': ("'none'",), + 'child-src': (RECAPTCHA_URL,), + 'connect-src': ( + "'self'", + GOOGLE_ANALYTICS_HOST, + GOOGLE_ADDITIONAL_ANALYTICS_HOST, + GOOGLE_TAGMANAGER_HOST, + ), + 'font-src': ("'self'", PROD_STATIC_URL), + 'form-action': ("'self'",), + 'frame-src': (RECAPTCHA_URL,), + 'img-src': ( + "'self'", + 'blob:', # Needed for image uploads. + 'data:', # Needed for theme wizard. + PROD_STATIC_URL, + PROD_MEDIA_URL, + GOOGLE_ANALYTICS_HOST, + GOOGLE_TAGMANAGER_HOST, + ), + 'media-src': ('https://videos.cdn.mozilla.net',), + 'object-src': ("'none'",), + 'report-uri': '/__cspreport__', + 'script-src': ( + GOOGLE_ANALYTICS_HOST, + GOOGLE_TAGMANAGER_HOST, + RECAPTCHA_URL, + 'https://www.gstatic.com/recaptcha/', + 'https://www.gstatic.cn/recaptcha/', + PROD_STATIC_URL, + ), + 'style-src': ("'unsafe-inline'", PROD_STATIC_URL), + }, + 'EXCLUDE_URL_PREFIXES': (), } # Should robots.txt deny everything or disallow a calculated list of URLs we