MPP-3838: restore safer CSP
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:
Родитель
687d385493
Коммит
7f0fb8b328
|
@ -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.*",
|
||||||
|
|
Загрузка…
Ссылка в новой задаче