зеркало из https://github.com/mozilla/bedrock.git
Support configurable CSP reporting percentage
This commit is contained in:
Родитель
fe9144dc0e
Коммит
6da5de9219
|
@ -24,7 +24,7 @@ from django.utils.deprecation import MiddlewareMixin
|
|||
from django.utils.translation import trans_real
|
||||
|
||||
from commonware.middleware import FrameOptionsHeader as OldFrameOptionsHeader
|
||||
from csp.middleware import CSPMiddleware
|
||||
from csp.contrib.rate_limiting import RateLimitedCSPMiddleware
|
||||
|
||||
from bedrock.base import metrics
|
||||
from bedrock.base.i18n import (
|
||||
|
@ -299,7 +299,7 @@ class MetricsViewTimingMiddleware(MiddlewareMixin):
|
|||
self._record_timing(request, 500)
|
||||
|
||||
|
||||
class CSPMiddlewareByPathPrefix(CSPMiddleware):
|
||||
class CSPMiddlewareByPathPrefix(RateLimitedCSPMiddleware):
|
||||
"""
|
||||
A subclass of CSPMiddleware that allows for different CSP policies based path prefix.
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.test import Client, RequestFactory, TestCase
|
|||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
import csp.constants
|
||||
import pytest
|
||||
from jinja2.exceptions import UndefinedError
|
||||
from markus.testing import MetricsMock
|
||||
|
@ -275,20 +276,24 @@ def csp_middleware():
|
|||
return CSPMiddlewareByPathPrefix(lambda req: HttpResponse())
|
||||
|
||||
|
||||
@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["default.com"]}})
|
||||
@override_settings(
|
||||
CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["default.com"]}},
|
||||
CONTENT_SECURITY_POLICY_REPORT_ONLY=None,
|
||||
)
|
||||
def test_no_csp_path_overrides(csp_middleware):
|
||||
rf = RequestFactory()
|
||||
request = rf.get("/u/thedude/")
|
||||
response = csp_middleware.process_response(request, HttpResponse())
|
||||
assert not hasattr(response, "_csp_config")
|
||||
assert not hasattr(response, "_csp_config_ro")
|
||||
assert "Content-Security-Policy" in response.headers
|
||||
assert "Content-Security-Policy-Report-Only" not in response.headers
|
||||
assert response.headers["Content-Security-Policy"] == "default-src default.com"
|
||||
assert csp.constants.HEADER in response.headers
|
||||
assert csp.constants.HEADER_REPORT_ONLY not in response.headers
|
||||
assert response.headers[csp.constants.HEADER] == "default-src default.com"
|
||||
|
||||
|
||||
@override_settings(
|
||||
CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["default.com"]}},
|
||||
CONTENT_SECURITY_POLICY_REPORT_ONLY=None,
|
||||
CSP_PATH_OVERRIDES={"/u/thedude": {"DIRECTIVES": {"default-src": ["override.com"]}}},
|
||||
)
|
||||
def test_csp_path_overrides(csp_middleware):
|
||||
|
@ -297,20 +302,24 @@ def test_csp_path_overrides(csp_middleware):
|
|||
response = csp_middleware.process_response(request, HttpResponse())
|
||||
assert response._csp_config == {"default-src": ["override.com"]}
|
||||
assert not hasattr(response, "_csp_config_ro")
|
||||
assert "Content-Security-Policy" in response.headers
|
||||
assert "Content-Security-Policy-Report-Only" not in response.headers
|
||||
assert response.headers["Content-Security-Policy"] == "default-src override.com"
|
||||
assert csp.constants.HEADER in response.headers
|
||||
assert csp.constants.HEADER_REPORT_ONLY not in response.headers
|
||||
assert response.headers[csp.constants.HEADER] == "default-src override.com"
|
||||
|
||||
|
||||
@override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["default.com"]}}, CSP_PATH_OVERRIDES={"/u/thedude": {"DIRECTIVES": {}}})
|
||||
@override_settings(
|
||||
CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["default.com"]}},
|
||||
CONTENT_SECURITY_POLICY_REPORT_ONLY=None,
|
||||
CSP_PATH_OVERRIDES={"/u/thedude": {"DIRECTIVES": {}}},
|
||||
)
|
||||
def test_csp_path_overrides_nullify(csp_middleware):
|
||||
rf = RequestFactory()
|
||||
request = rf.get("/u/thedude/")
|
||||
response = csp_middleware.process_response(request, HttpResponse())
|
||||
assert response._csp_config == {}
|
||||
assert not hasattr(response, "_csp_config_ro")
|
||||
assert "Content-Security-Policy" not in response.headers
|
||||
assert "Content-Security-Policy-Report-Only" not in response.headers
|
||||
assert csp.constants.HEADER not in response.headers
|
||||
assert csp.constants.HEADER_REPORT_ONLY not in response.headers
|
||||
|
||||
|
||||
@override_settings(
|
||||
|
@ -324,7 +333,21 @@ def test_csp_path_overrides_report_only(csp_middleware):
|
|||
response = csp_middleware.process_response(request, HttpResponse())
|
||||
assert response._csp_config_ro == {"default-src": ["override.com"]}
|
||||
assert not hasattr(response, "_csp_config")
|
||||
assert "Content-Security-Policy" in response.headers
|
||||
assert "Content-Security-Policy-Report-Only" in response.headers
|
||||
assert response.headers["Content-Security-Policy"] == "default-src default.com"
|
||||
assert response.headers["Content-Security-Policy-Report-Only"] == "default-src override.com"
|
||||
assert csp.constants.HEADER in response.headers
|
||||
assert csp.constants.HEADER_REPORT_ONLY in response.headers
|
||||
assert response.headers[csp.constants.HEADER] == "default-src default.com"
|
||||
assert response.headers[csp.constants.HEADER_REPORT_ONLY] == "default-src override.com"
|
||||
|
||||
|
||||
@override_settings(
|
||||
CONTENT_SECURITY_POLICY={"REPORT_PERCENTAGE": 0, "DIRECTIVES": {"default-src": ["default.com"]}},
|
||||
CONTENT_SECURITY_POLICY_REPORT_ONLY={"REPORT_PERCENTAGE": 100, "DIRECTIVES": {"default-src": ["default.com"], "report-uri": ["report.com"]}},
|
||||
)
|
||||
def test_csp_report_percentage_zero(csp_middleware):
|
||||
rf = RequestFactory()
|
||||
request = rf.get("/u/thedude/")
|
||||
response = csp_middleware.process_response(request, HttpResponse())
|
||||
assert csp.constants.HEADER in response.headers
|
||||
assert csp.constants.HEADER_REPORT_ONLY in response.headers
|
||||
assert "report-uri" not in response.headers[csp.constants.HEADER]
|
||||
assert "report-uri" in response.headers[csp.constants.HEADER_REPORT_ONLY]
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import logging.config
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
|
||||
from csp.constants import SELF, UNSAFE_EVAL, UNSAFE_INLINE
|
||||
|
||||
|
@ -258,15 +259,21 @@ CONTENT_SECURITY_POLICY = {
|
|||
"connect-src": list(set(_csp_default_src + _csp_connect_src)),
|
||||
# support older browsers (mainly Safari)
|
||||
"frame-src": _csp_child_src,
|
||||
"report-uri": config("CSP_REPORT_URI", default="") or None,
|
||||
},
|
||||
}
|
||||
|
||||
# Start report-only CSP as a copy. We'll modify it later if needed.
|
||||
# Only set up report-only CSP if we have a report-uri set.
|
||||
if csp_report_uri := config("CSP_REPORT_URI", default="") or None:
|
||||
CONTENT_SECURITY_POLICY_REPORT_ONLY = deepcopy(CONTENT_SECURITY_POLICY)
|
||||
CONTENT_SECURITY_POLICY_REPORT_ONLY["REPORT_PERCENTAGE"] = config("CSP_REPORT_PERCENTAGE", default="100", parser=int)
|
||||
CONTENT_SECURITY_POLICY_REPORT_ONLY["DIRECTIVES"]["report-uri"] = csp_report_uri
|
||||
|
||||
# Mainly for overriding CSP settings for CMS admin.
|
||||
# Works in conjunction with the `bedrock.base.middleware.CSPMiddlewareByPathPrefix` middleware.
|
||||
|
||||
# /cms-admin/images/ loads just-uploaded images as blobs.
|
||||
CMS_ADMIN_IMAGES_CSP = CONTENT_SECURITY_POLICY.copy()
|
||||
CMS_ADMIN_IMAGES_CSP = deepcopy(CONTENT_SECURITY_POLICY)
|
||||
CMS_ADMIN_IMAGES_CSP["DIRECTIVES"]["img-src"] += ["blob:"]
|
||||
|
||||
CSP_PATH_OVERRIDES = {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import click
|
||||
import requests
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("-n", type=click.IntRange(0, 100, clamp=True), default=100, help="Number of requests to make")
|
||||
@click.argument("url", type=str, required=True)
|
||||
def check_for_report_uri(url, n=100):
|
||||
"""
|
||||
Check if a URL has a Content-Security-Policy header with a report-uri directive.
|
||||
"""
|
||||
print(f"Checking {url}")
|
||||
results = defaultdict(int)
|
||||
for _ in range(n):
|
||||
resp = requests.get(url)
|
||||
|
||||
if "Content-Security-Policy" in resp.headers:
|
||||
header = resp.headers["Content-Security-Policy-Report-Only"]
|
||||
if "report-uri" in header:
|
||||
results["report-uri present"] += 1
|
||||
else:
|
||||
results["report-uri not present"] += 1
|
||||
else:
|
||||
results["No CSP header"] += 1
|
||||
|
||||
print(f"{n} requests made")
|
||||
print(results)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_for_report_uri()
|
|
@ -11,7 +11,6 @@ ALLOWED_HOSTS: .allizom.org,.moz.works,.run.app
|
|||
CONTENT_CARDS_URL: https://www-dev.allizom.org/media/
|
||||
CSP_DEFAULT_SRC: "*.allizom.org"
|
||||
CSP_EXTRA_FRAME_SRC: "*.mozaws.net,o1069899.sentry.io"
|
||||
CSP_REPORT_ENABLE: "True"
|
||||
DB_DOWNLOAD_IGNORE_GIT: "True"
|
||||
DEBUG: "False"
|
||||
DEV: "True"
|
||||
|
|
Загрузка…
Ссылка в новой задаче