зеркало из 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 django.utils.translation import trans_real
|
||||||
|
|
||||||
from commonware.middleware import FrameOptionsHeader as OldFrameOptionsHeader
|
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 import metrics
|
||||||
from bedrock.base.i18n import (
|
from bedrock.base.i18n import (
|
||||||
|
@ -299,7 +299,7 @@ class MetricsViewTimingMiddleware(MiddlewareMixin):
|
||||||
self._record_timing(request, 500)
|
self._record_timing(request, 500)
|
||||||
|
|
||||||
|
|
||||||
class CSPMiddlewareByPathPrefix(CSPMiddleware):
|
class CSPMiddlewareByPathPrefix(RateLimitedCSPMiddleware):
|
||||||
"""
|
"""
|
||||||
A subclass of CSPMiddleware that allows for different CSP policies based path prefix.
|
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.test.utils import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
import csp.constants
|
||||||
import pytest
|
import pytest
|
||||||
from jinja2.exceptions import UndefinedError
|
from jinja2.exceptions import UndefinedError
|
||||||
from markus.testing import MetricsMock
|
from markus.testing import MetricsMock
|
||||||
|
@ -275,20 +276,24 @@ def csp_middleware():
|
||||||
return CSPMiddlewareByPathPrefix(lambda req: HttpResponse())
|
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):
|
def test_no_csp_path_overrides(csp_middleware):
|
||||||
rf = RequestFactory()
|
rf = RequestFactory()
|
||||||
request = rf.get("/u/thedude/")
|
request = rf.get("/u/thedude/")
|
||||||
response = csp_middleware.process_response(request, HttpResponse())
|
response = csp_middleware.process_response(request, HttpResponse())
|
||||||
assert not hasattr(response, "_csp_config")
|
assert not hasattr(response, "_csp_config")
|
||||||
assert not hasattr(response, "_csp_config_ro")
|
assert not hasattr(response, "_csp_config_ro")
|
||||||
assert "Content-Security-Policy" in response.headers
|
assert csp.constants.HEADER in response.headers
|
||||||
assert "Content-Security-Policy-Report-Only" not in response.headers
|
assert csp.constants.HEADER_REPORT_ONLY not in response.headers
|
||||||
assert response.headers["Content-Security-Policy"] == "default-src default.com"
|
assert response.headers[csp.constants.HEADER] == "default-src default.com"
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["default.com"]}},
|
CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["default.com"]}},
|
||||||
|
CONTENT_SECURITY_POLICY_REPORT_ONLY=None,
|
||||||
CSP_PATH_OVERRIDES={"/u/thedude": {"DIRECTIVES": {"default-src": ["override.com"]}}},
|
CSP_PATH_OVERRIDES={"/u/thedude": {"DIRECTIVES": {"default-src": ["override.com"]}}},
|
||||||
)
|
)
|
||||||
def test_csp_path_overrides(csp_middleware):
|
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())
|
response = csp_middleware.process_response(request, HttpResponse())
|
||||||
assert response._csp_config == {"default-src": ["override.com"]}
|
assert response._csp_config == {"default-src": ["override.com"]}
|
||||||
assert not hasattr(response, "_csp_config_ro")
|
assert not hasattr(response, "_csp_config_ro")
|
||||||
assert "Content-Security-Policy" in response.headers
|
assert csp.constants.HEADER in response.headers
|
||||||
assert "Content-Security-Policy-Report-Only" not in response.headers
|
assert csp.constants.HEADER_REPORT_ONLY not in response.headers
|
||||||
assert response.headers["Content-Security-Policy"] == "default-src override.com"
|
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):
|
def test_csp_path_overrides_nullify(csp_middleware):
|
||||||
rf = RequestFactory()
|
rf = RequestFactory()
|
||||||
request = rf.get("/u/thedude/")
|
request = rf.get("/u/thedude/")
|
||||||
response = csp_middleware.process_response(request, HttpResponse())
|
response = csp_middleware.process_response(request, HttpResponse())
|
||||||
assert response._csp_config == {}
|
assert response._csp_config == {}
|
||||||
assert not hasattr(response, "_csp_config_ro")
|
assert not hasattr(response, "_csp_config_ro")
|
||||||
assert "Content-Security-Policy" not in response.headers
|
assert csp.constants.HEADER not in response.headers
|
||||||
assert "Content-Security-Policy-Report-Only" not in response.headers
|
assert csp.constants.HEADER_REPORT_ONLY not in response.headers
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
|
@ -324,7 +333,21 @@ def test_csp_path_overrides_report_only(csp_middleware):
|
||||||
response = csp_middleware.process_response(request, HttpResponse())
|
response = csp_middleware.process_response(request, HttpResponse())
|
||||||
assert response._csp_config_ro == {"default-src": ["override.com"]}
|
assert response._csp_config_ro == {"default-src": ["override.com"]}
|
||||||
assert not hasattr(response, "_csp_config")
|
assert not hasattr(response, "_csp_config")
|
||||||
assert "Content-Security-Policy" in response.headers
|
assert csp.constants.HEADER in response.headers
|
||||||
assert "Content-Security-Policy-Report-Only" in response.headers
|
assert csp.constants.HEADER_REPORT_ONLY in response.headers
|
||||||
assert response.headers["Content-Security-Policy"] == "default-src default.com"
|
assert response.headers[csp.constants.HEADER] == "default-src default.com"
|
||||||
assert response.headers["Content-Security-Policy-Report-Only"] == "default-src override.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 logging.config
|
||||||
import sys
|
import sys
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
from csp.constants import SELF, UNSAFE_EVAL, UNSAFE_INLINE
|
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)),
|
"connect-src": list(set(_csp_default_src + _csp_connect_src)),
|
||||||
# support older browsers (mainly Safari)
|
# support older browsers (mainly Safari)
|
||||||
"frame-src": _csp_child_src,
|
"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.
|
# Mainly for overriding CSP settings for CMS admin.
|
||||||
# Works in conjunction with the `bedrock.base.middleware.CSPMiddlewareByPathPrefix` middleware.
|
# Works in conjunction with the `bedrock.base.middleware.CSPMiddlewareByPathPrefix` middleware.
|
||||||
|
|
||||||
# /cms-admin/images/ loads just-uploaded images as blobs.
|
# /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:"]
|
CMS_ADMIN_IMAGES_CSP["DIRECTIVES"]["img-src"] += ["blob:"]
|
||||||
|
|
||||||
CSP_PATH_OVERRIDES = {
|
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/
|
CONTENT_CARDS_URL: https://www-dev.allizom.org/media/
|
||||||
CSP_DEFAULT_SRC: "*.allizom.org"
|
CSP_DEFAULT_SRC: "*.allizom.org"
|
||||||
CSP_EXTRA_FRAME_SRC: "*.mozaws.net,o1069899.sentry.io"
|
CSP_EXTRA_FRAME_SRC: "*.mozaws.net,o1069899.sentry.io"
|
||||||
CSP_REPORT_ENABLE: "True"
|
|
||||||
DB_DOWNLOAD_IGNORE_GIT: "True"
|
DB_DOWNLOAD_IGNORE_GIT: "True"
|
||||||
DEBUG: "False"
|
DEBUG: "False"
|
||||||
DEV: "True"
|
DEV: "True"
|
||||||
|
|
Загрузка…
Ссылка в новой задаче