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

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

@ -1,3 +1,5 @@
import binascii
import os
import re
import time
from collections.abc import Callable
@ -8,11 +10,61 @@ from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
import markus
from csp.middleware import CSPMiddleware
from whitenoise.middleware import WhiteNoiseMiddleware
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:
def __init__(self, get_response):
self.get_response = get_response

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

@ -26,7 +26,7 @@ import dj_database_url
import django_stubs_ext
import markus
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 sentry_sdk.integrations.django import DjangoIntegration
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
_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
# 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],
"connect-src": [
SELF,
"https://www.google-analytics.com/",
"https://www.googletagmanager.com/",
"https://*.google-analytics.com",
"https://*.analytics.google.com",
"https://*.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],
"img-src": [
SELF,
"https://*.google-analytics.com",
"https://*.googletagmanager.com",
],
"object-src": [NONE],
"script-src": [
SELF,
UNSAFE_INLINE, # TODO: remove this temporary fix for GA4
NONCE,
"https://www.google-analytics.com/",
"https://www.googletagmanager.com/",
"https://*.googletagmanager.com",
"https://js.stripe.com/",
],
"style-src": [SELF],
@ -373,7 +379,7 @@ if DEBUG:
MIDDLEWARE += [
"django.middleware.security.SecurityMiddleware",
"csp.middleware.CSPMiddleware",
"privaterelay.middleware.EagerNonceCSPMiddleware",
"privaterelay.middleware.RedirectRootIfLoggedIn",
"privaterelay.middleware.RelayStaticFilesMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",

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

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