From 8dc4c453b01730c89bcfdfd253f3d97e2d479583 Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 20 Jun 2024 15:24:24 -0500 Subject: [PATCH 1/5] Update to django-csp 4.0b1 This release is still in beta, and includes a change in the CSP configuration. --- privaterelay/settings.py | 68 ++++++++++++++++++++++------------------ privaterelay/types.py | 56 ++++++++++++++++++++++++++++++++- requirements.txt | 2 +- 3 files changed, 93 insertions(+), 33 deletions(-) diff --git a/privaterelay/settings.py b/privaterelay/settings.py index 82c95df419..c28a5b550b 100644 --- a/privaterelay/settings.py +++ b/privaterelay/settings.py @@ -30,7 +30,7 @@ from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import ignore_logger -from .types import RELAY_CHANNEL_NAME +from .types import CONTENT_SECURITY_POLICY_T, RELAY_CHANNEL_NAME if TYPE_CHECKING: import wsgiref.headers @@ -183,36 +183,41 @@ empty_hash = base64.b64encode(sha256().digest()).decode() _CSP_STYLE_HASHES.append(f"'sha256-{empty_hash}'") -CSP_DEFAULT_SRC = ["'self'"] -CSP_CONNECT_SRC = [ - "'self'", - "https://www.google-analytics.com/", - "https://www.googletagmanager.com/", - "https://location.services.mozilla.com", - "https://api.stripe.com", - BASKET_ORIGIN, -] + _ACCOUNT_CONNECT_SRC -CSP_FONT_SRC = ["'self'"] + _API_DOCS_CSP_FONT_SRC + ["https://relay.firefox.com/"] -CSP_IMG_SRC = ["'self'"] + _AVATAR_IMG_SRC + _API_DOCS_CSP_IMG_SRC -CSP_SCRIPT_SRC = ( - ["'self'"] - + (["'unsafe-inline'"] if _CSP_SCRIPT_INLINE else []) - + [ - "https://www.google-analytics.com/", - "https://www.googletagmanager.com/", - "https://js.stripe.com/", - ] -) -CSP_WORKER_SRC = _API_DOCS_CSP_WORKER_SRC or None -CSP_OBJECT_SRC = ["'none'"] -CSP_FRAME_SRC = ["https://js.stripe.com", "https://hooks.stripe.com"] -CSP_STYLE_SRC = ( - ["'self'"] - + (["'unsafe-inline'"] if _CSP_STYLE_INLINE else []) - + _API_DOCS_CSP_STYLE_SRC - + _CSP_STYLE_HASHES -) -CSP_REPORT_URI = config("CSP_REPORT_URI", "") +CONTENT_SECURITY_POLICY: CONTENT_SECURITY_POLICY_T = { + "DIRECTIVES": { + "report-uri": config("CSP_REPORT_URI", ""), + "default-src": ["'self'"], + "connect-src": [ + "'self'", + "https://www.google-analytics.com/", + "https://www.googletagmanager.com/", + "https://location.services.mozilla.com", + "https://api.stripe.com", + BASKET_ORIGIN, + ], + "font-src": ["'self'", "https://relay.firefox.com/"], + "frame-src": ["https://js.stripe.com", "https://hooks.stripe.com"], + "img-src": ["'self'"], + "object-src": ["'none'"], + "script-src": [ + "'self'", + "https://www.google-analytics.com/", + "https://www.googletagmanager.com/", + "https://js.stripe.com/", + ], + "style-src": ["'self'"], + } +} +CONTENT_SECURITY_POLICY["DIRECTIVES"]["connect-src"].extend(_ACCOUNT_CONNECT_SRC) +CONTENT_SECURITY_POLICY["DIRECTIVES"]["font-src"].extend(_API_DOCS_CSP_FONT_SRC) +CONTENT_SECURITY_POLICY["DIRECTIVES"]["img-src"].extend(_AVATAR_IMG_SRC) +CONTENT_SECURITY_POLICY["DIRECTIVES"]["img-src"].extend(_API_DOCS_CSP_IMG_SRC) +CONTENT_SECURITY_POLICY["DIRECTIVES"]["style-src"].extend(_API_DOCS_CSP_STYLE_SRC) +CONTENT_SECURITY_POLICY["DIRECTIVES"]["style-src"].extend(_CSP_STYLE_HASHES) +if _CSP_STYLE_INLINE: + CONTENT_SECURITY_POLICY["DIRECTIVES"]["script-src"].append("'unsafe-inline'") +if _API_DOCS_CSP_WORKER_SRC: + CONTENT_SECURITY_POLICY["DIRECTIVES"]["worker-src"] = _API_DOCS_CSP_WORKER_SRC REFERRER_POLICY = "strict-origin-when-cross-origin" @@ -317,6 +322,7 @@ "rest_framework", "rest_framework.authtoken", "corsheaders", + "csp", "waffle", "privaterelay.apps.PrivateRelayConfig", "api.apps.ApiConfig", diff --git a/privaterelay/types.py b/privaterelay/types.py index 9a1f5fb310..a863dee2e2 100644 --- a/privaterelay/types.py +++ b/privaterelay/types.py @@ -1,5 +1,59 @@ """Types for the privaterelay app""" -from typing import Literal +from typing import Literal, TypedDict RELAY_CHANNEL_NAME = Literal["local", "dev", "stage", "prod"] + +# django-csp 4.0: types for CONTENT_SECURITY_POLICY in settings.py + +# See https://github.com/mozilla/django-csp/blob/main/csp/utils.py +CSP_DIRECTIVES_T = TypedDict( + "CSP_DIRECTIVES_T", + { + # Fetch Directives + "child-src": list[str], + "connect-src": list[str], + "default-src": list[str], + "script-src": list[str], + "script-src-attr": list[str], + "script-src-elem": list[str], + "object-src": list[str], + "style-src": list[str], + "style-src-attr": list[str], + "style-src-elem": list[str], + "font-src": list[str], + "frame-src": list[str], + "img-src": list[str], + "manifest-src": list[str], + "media-src": list[str], + "prefetch-src": list[str], # Deprecated. + # Document Directives + "base-uri": list[str], + "plugin-types": list[str], # Deprecated. + "sandbox": list[str], + # Navigation Directives + "form-action": list[str], + "frame-ancestors": list[str], + "navigate-to": list[str], + # Reporting Directives + "report-uri": str, + "report-to": list[str], + "require-sri-for": list[str], + # Trusted Types Directives + "require-trusted-types-for": list[str], + "trusted-types": list[str], + # Other Directives + "webrtc": list[str], + "worker-src": list[str], + # Directives Defined in Other Documents + "upgrade-insecure-requests": list[str], + "block-all-mixed-content": list[str], # Deprecated. + }, + total=False, +) + + +class CONTENT_SECURITY_POLICY_T(TypedDict, total=False): + EXCLUDE_URL_PREFIXES: list[str] + DIRECTIVES: CSP_DIRECTIVES_T + REPORT_PERCENTAGE: int diff --git a/requirements.txt b/requirements.txt index 0861e08256..0eb9300d38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ Django==4.2.13 dj-database-url==2.2.0 django-allauth[socialaccount]==0.63.3 django-cors-headers==4.4.0 -django-csp==3.8 +django-csp==4.0b1 django-debug-toolbar==4.4.2 django-filter==24.2 django-ipware==7.0.1 From 9422347b6d5bbeda1513f29d077dc2f4ae11803f Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 20 Jun 2024 15:29:12 -0500 Subject: [PATCH 2/5] Use csp.constants --- privaterelay/settings.py | 17 +++++++++-------- pyproject.toml | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/privaterelay/settings.py b/privaterelay/settings.py index c28a5b550b..7649c2e027 100644 --- a/privaterelay/settings.py +++ b/privaterelay/settings.py @@ -26,6 +26,7 @@ import django_stubs_ext import markus import sentry_sdk +from csp.constants import NONE, SELF, UNSAFE_INLINE from decouple import Choices, Csv, config from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import ignore_logger @@ -186,26 +187,26 @@ CONTENT_SECURITY_POLICY: CONTENT_SECURITY_POLICY_T = { "DIRECTIVES": { "report-uri": config("CSP_REPORT_URI", ""), - "default-src": ["'self'"], + "default-src": [SELF], "connect-src": [ - "'self'", + SELF, "https://www.google-analytics.com/", "https://www.googletagmanager.com/", "https://location.services.mozilla.com", "https://api.stripe.com", BASKET_ORIGIN, ], - "font-src": ["'self'", "https://relay.firefox.com/"], + "font-src": [SELF, "https://relay.firefox.com/"], "frame-src": ["https://js.stripe.com", "https://hooks.stripe.com"], - "img-src": ["'self'"], - "object-src": ["'none'"], + "img-src": [SELF], + "object-src": [NONE], "script-src": [ - "'self'", + SELF, "https://www.google-analytics.com/", "https://www.googletagmanager.com/", "https://js.stripe.com/", ], - "style-src": ["'self'"], + "style-src": [SELF], } } CONTENT_SECURITY_POLICY["DIRECTIVES"]["connect-src"].extend(_ACCOUNT_CONNECT_SRC) @@ -215,7 +216,7 @@ CONTENT_SECURITY_POLICY["DIRECTIVES"]["style-src"].extend(_API_DOCS_CSP_STYLE_SRC) CONTENT_SECURITY_POLICY["DIRECTIVES"]["style-src"].extend(_CSP_STYLE_HASHES) if _CSP_STYLE_INLINE: - CONTENT_SECURITY_POLICY["DIRECTIVES"]["script-src"].append("'unsafe-inline'") + CONTENT_SECURITY_POLICY["DIRECTIVES"]["script-src"].append(UNSAFE_INLINE) if _API_DOCS_CSP_WORKER_SRC: CONTENT_SECURITY_POLICY["DIRECTIVES"]["worker-src"] = _API_DOCS_CSP_WORKER_SRC diff --git a/pyproject.toml b/pyproject.toml index 548a5546b5..7ce94a235c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ module = [ "botocore.config", "botocore.exceptions", "codetiming", + "csp.constants", "debug_toolbar", "dj_database_url", "django_filters.*", From a3a23250b04503544b392bfc949a1bd0d1c2f11a Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 20 Jun 2024 15:33:27 -0500 Subject: [PATCH 3/5] Do not set report-uri if blank. --- privaterelay/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/privaterelay/settings.py b/privaterelay/settings.py index 7649c2e027..d46ad9bd93 100644 --- a/privaterelay/settings.py +++ b/privaterelay/settings.py @@ -186,7 +186,6 @@ CONTENT_SECURITY_POLICY: CONTENT_SECURITY_POLICY_T = { "DIRECTIVES": { - "report-uri": config("CSP_REPORT_URI", ""), "default-src": [SELF], "connect-src": [ SELF, @@ -219,6 +218,8 @@ CONTENT_SECURITY_POLICY["DIRECTIVES"]["script-src"].append(UNSAFE_INLINE) if _API_DOCS_CSP_WORKER_SRC: CONTENT_SECURITY_POLICY["DIRECTIVES"]["worker-src"] = _API_DOCS_CSP_WORKER_SRC +if _CSP_REPORT_URI := config("CSP_REPORT_URI", ""): + CONTENT_SECURITY_POLICY["DIRECTIVES"]["report-uri"] = _CSP_REPORT_URI REFERRER_POLICY = "strict-origin-when-cross-origin" From 5a2d108f0be138d8272e4c018cd8ef82cc02a0a6 Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Thu, 20 Jun 2024 16:41:53 -0500 Subject: [PATCH 4/5] Fix types for some directives --- privaterelay/types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/privaterelay/types.py b/privaterelay/types.py index a863dee2e2..0c36475a02 100644 --- a/privaterelay/types.py +++ b/privaterelay/types.py @@ -46,8 +46,8 @@ "webrtc": list[str], "worker-src": list[str], # Directives Defined in Other Documents - "upgrade-insecure-requests": list[str], - "block-all-mixed-content": list[str], # Deprecated. + "upgrade-insecure-requests": bool, + "block-all-mixed-content": bool, # Deprecated. }, total=False, ) From 911e86e58edcaa122bae06a01eab76bef7bf690d Mon Sep 17 00:00:00 2001 From: John Whitlock Date: Fri, 21 Jun 2024 10:52:01 -0500 Subject: [PATCH 5/5] More type adjustments from CSP 3 --- privaterelay/types.py | 86 ++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/privaterelay/types.py b/privaterelay/types.py index 0c36475a02..613be287db 100644 --- a/privaterelay/types.py +++ b/privaterelay/types.py @@ -6,48 +6,58 @@ # django-csp 4.0: types for CONTENT_SECURITY_POLICY in settings.py -# See https://github.com/mozilla/django-csp/blob/main/csp/utils.py +# Note: this will need adjustments to uplift to django-csp +# For example, the django-csp docs say 'sequence' rather than 'list', +# and appear more flexible about sending strings or lists. +_SERIALIZED_SOURCE_LIST = list[str] CSP_DIRECTIVES_T = TypedDict( "CSP_DIRECTIVES_T", { - # Fetch Directives - "child-src": list[str], - "connect-src": list[str], - "default-src": list[str], - "script-src": list[str], - "script-src-attr": list[str], - "script-src-elem": list[str], - "object-src": list[str], - "style-src": list[str], - "style-src-attr": list[str], - "style-src-elem": list[str], - "font-src": list[str], - "frame-src": list[str], - "img-src": list[str], - "manifest-src": list[str], - "media-src": list[str], - "prefetch-src": list[str], # Deprecated. - # Document Directives - "base-uri": list[str], - "plugin-types": list[str], # Deprecated. - "sandbox": list[str], - # Navigation Directives - "form-action": list[str], - "frame-ancestors": list[str], - "navigate-to": list[str], - # Reporting Directives - "report-uri": str, - "report-to": list[str], - "require-sri-for": list[str], - # Trusted Types Directives - "require-trusted-types-for": list[str], - "trusted-types": list[str], - # Other Directives - "webrtc": list[str], - "worker-src": list[str], - # Directives Defined in Other Documents - "upgrade-insecure-requests": bool, + # CSP Level 3 Working Draft, Directives (section 6) + # https://www.w3.org/TR/CSP/#csp-directives + # 6.1 Fetch Directives + "child-src": _SERIALIZED_SOURCE_LIST, + "connect-src": _SERIALIZED_SOURCE_LIST, + "default-src": _SERIALIZED_SOURCE_LIST, + "font-src": _SERIALIZED_SOURCE_LIST, + "frame-src": _SERIALIZED_SOURCE_LIST, + "img-src": _SERIALIZED_SOURCE_LIST, + "manifest-src": _SERIALIZED_SOURCE_LIST, + "media-src": _SERIALIZED_SOURCE_LIST, + "object-src": _SERIALIZED_SOURCE_LIST, + "script-src": _SERIALIZED_SOURCE_LIST, + "script-src-elem": _SERIALIZED_SOURCE_LIST, + "script-src-attr": _SERIALIZED_SOURCE_LIST, + "style-src": _SERIALIZED_SOURCE_LIST, + "style-src-elem": _SERIALIZED_SOURCE_LIST, + "style-src-attr": _SERIALIZED_SOURCE_LIST, + # 6.2 Other Directives + "webrtc": Literal["'allow'", "'block'"], + "worker-src": _SERIALIZED_SOURCE_LIST, + # 6.3 Document Directives + "base-uri": _SERIALIZED_SOURCE_LIST, + "sandbox": str | list[str], # sequence of tokens in CSP 3 + # 6.4 Navigation Directives + "form-action": _SERIALIZED_SOURCE_LIST, + "frame-ancestors": _SERIALIZED_SOURCE_LIST, + "navigate-to": _SERIALIZED_SOURCE_LIST, + # 6.5 Reporting Directives + "report-uri": str | list[str], # sequence of uri-references in CSP 3 + "report-to": str, + # "require-sri-for": _SERIALIZED_SOURCE_LIST, + # 6.6 Directives Defined in Other Documents "block-all-mixed-content": bool, # Deprecated. + "upgrade-insecure-requests": bool, + # CSP2 items removed in CSP3 + # https://www.w3.org/TR/CSP2/#directives + "plugin-types": _SERIALIZED_SOURCE_LIST, + # Deprecated, from MDN + "prefetch-src": _SERIALIZED_SOURCE_LIST, + "referrer": str, + # Experimental items, from MDN + "fenced-frame-src": _SERIALIZED_SOURCE_LIST, + "require-trusted-types-for": str, + "trusted-types": str, }, total=False, )