Use a new EagerNonceCSPMiddleware to add nonce to the CSP and update the
React app to include it in dynamic scripts.
This commit is contained in:
groovecoder 2024-07-03 10:38:37 -05:00
Родитель 687d385493
Коммит 7f0fb8b328
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4825AB58E974B712
4 изменённых файлов: 68 добавлений и 7 удалений

Просмотреть файл

@ -34,6 +34,7 @@ import { HolidayPromoBanner } from "./topmessage/HolidayPromoBanner";
import { isFlagActive } from "../../functions/waffle"; import { isFlagActive } from "../../functions/waffle";
import { useMetrics } from "../../hooks/metrics"; import { useMetrics } from "../../hooks/metrics";
import { GoogleAnalyticsWorkaround } from "../GoogleAnalyticsWorkaround"; import { GoogleAnalyticsWorkaround } from "../GoogleAnalyticsWorkaround";
import { getCookie } from "../../functions/cookies";
export type Props = { export type Props = {
children: ReactNode; children: ReactNode;
@ -293,6 +294,7 @@ export const Layout = (props: Props) => {
metricsEnabled === "enabled" ? ( metricsEnabled === "enabled" ? (
<GoogleAnalyticsWorkaround <GoogleAnalyticsWorkaround
gaId={props.runtimeData.GA4_MEASUREMENT_ID} gaId={props.runtimeData.GA4_MEASUREMENT_ID}
nonce={getCookie("csp_nonce")}
debugMode={ debugMode={
process.env.NEXT_PUBLIC_GA4_DEBUG_MODE === "true" && process.env.NEXT_PUBLIC_GA4_DEBUG_MODE === "true" &&
process.env.NODE_ENV !== "test" process.env.NODE_ENV !== "test"

Просмотреть файл

@ -1,3 +1,5 @@
import binascii
import os
import re import re
import time import time
from collections.abc import Callable from collections.abc import Callable
@ -8,11 +10,61 @@ from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
import markus import markus
from csp.middleware import CSPMiddleware
from whitenoise.middleware import WhiteNoiseMiddleware from whitenoise.middleware import WhiteNoiseMiddleware
metrics = markus.get_metrics("fx-private-relay") metrics = markus.get_metrics("fx-private-relay")
# To find all the URL paths that serve HTML which need the CSP nonce:
# python manage.py collectstatic
# find staticfiles -type f -name 'index.html'
CSP_NONCE_COOKIE_PATHS = [
"/",
"/contains-tracker-warning",
"/flags",
"/faq",
"/vpn-relay/waitlist",
"/accounts/settings",
"/accounts/profile",
"/accounts/account_inactive",
"/vpn-relay-welcome",
"/phone/waitlist",
"/phone",
"/404",
"/tracker-report",
"/premium/waitlist",
"/premium",
]
class EagerNonceCSPMiddleware(CSPMiddleware):
# We need a nonce to use Google Tag Manager with a safe CSP:
# https://developers.google.com/tag-platform/security/guides/csp
# django-csp only includes the nonce value in the CSP header if the csp_nonce
# attribute is accessed:
# https://django-csp.readthedocs.io/en/latest/nonce.html
# That works for urls served by Django views that access the attribute but it
# doesn't work for urls that are served by views which don't access the attribute.
# (e.g., Whitenoise)
# So, to ensure django-csp includes the nonce value in the CSP header of every
# response, we override the default CSPMiddleware with this middleware. If the
# request is for one of the HTML urls, this middleware sets the request.csp_nonce
# attribute and adds a cookie for the React app to get the nonce value for scripts.
def process_request(self, request):
if request.path in CSP_NONCE_COOKIE_PATHS:
request_nonce = binascii.hexlify(os.urandom(16)).decode("ascii")
request._csp_nonce = request_nonce
def process_response(self, request, response):
response = super().process_response(request, response)
if request.path in CSP_NONCE_COOKIE_PATHS:
response.set_cookie(
"csp_nonce", request._csp_nonce, secure=True, samesite="Strict"
)
return response
class RedirectRootIfLoggedIn: class RedirectRootIfLoggedIn:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response

Просмотреть файл

@ -26,7 +26,7 @@ import dj_database_url
import django_stubs_ext import django_stubs_ext
import markus import markus
import sentry_sdk import sentry_sdk
from csp.constants import NONE, SELF, UNSAFE_INLINE from csp.constants import NONCE, NONE, SELF, UNSAFE_INLINE
from decouple import Choices, Csv, config from decouple import Choices, Csv, config
from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.integrations.logging import ignore_logger
@ -141,6 +141,7 @@ else:
API_DOCS_ENABLED = config("API_DOCS_ENABLED", False, cast=bool) or DEBUG API_DOCS_ENABLED = config("API_DOCS_ENABLED", False, cast=bool) or DEBUG
_CSP_SCRIPT_INLINE = API_DOCS_ENABLED or USE_SILK _CSP_SCRIPT_INLINE = API_DOCS_ENABLED or USE_SILK
_CSP_SCRIPT_INLINE = False
# When running locally, styles might get refreshed while the server is running, so their # When running locally, styles might get refreshed while the server is running, so their
# hashes would get oudated. Hence, we just allow all of them. # hashes would get oudated. Hence, we just allow all of them.
@ -189,21 +190,26 @@ CONTENT_SECURITY_POLICY: CONTENT_SECURITY_POLICY_T = {
"default-src": [SELF], "default-src": [SELF],
"connect-src": [ "connect-src": [
SELF, SELF,
"https://www.google-analytics.com/", "https://*.google-analytics.com",
"https://www.googletagmanager.com/", "https://*.analytics.google.com",
"https://*.googletagmanager.com",
"https://location.services.mozilla.com", "https://location.services.mozilla.com",
"https://api.stripe.com", "https://api.stripe.com",
BASKET_ORIGIN, 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"], "frame-src": ["https://js.stripe.com", "https://hooks.stripe.com"],
"img-src": [SELF], "img-src": [
SELF,
"https://*.google-analytics.com",
"https://*.googletagmanager.com",
],
"object-src": [NONE], "object-src": [NONE],
"script-src": [ "script-src": [
SELF, SELF,
UNSAFE_INLINE, # TODO: remove this temporary fix for GA4 NONCE,
"https://www.google-analytics.com/", "https://www.google-analytics.com/",
"https://www.googletagmanager.com/", "https://*.googletagmanager.com",
"https://js.stripe.com/", "https://js.stripe.com/",
], ],
"style-src": [SELF], "style-src": [SELF],
@ -373,7 +379,7 @@ if DEBUG:
MIDDLEWARE += [ MIDDLEWARE += [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"csp.middleware.CSPMiddleware", "privaterelay.middleware.EagerNonceCSPMiddleware",
"privaterelay.middleware.RedirectRootIfLoggedIn", "privaterelay.middleware.RedirectRootIfLoggedIn",
"privaterelay.middleware.RelayStaticFilesMiddleware", "privaterelay.middleware.RelayStaticFilesMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",

Просмотреть файл

@ -51,6 +51,7 @@ module = [
"botocore.exceptions", "botocore.exceptions",
"codetiming", "codetiming",
"csp.constants", "csp.constants",
"csp.middleware",
"debug_toolbar", "debug_toolbar",
"dj_database_url", "dj_database_url",
"django_filters.*", "django_filters.*",