Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions pydotorg/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"csp.middleware.CSPMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"pydotorg.middleware.AdminNoCaching",
"pydotorg.middleware.GlobalSurrogateKey",
Expand Down Expand Up @@ -297,6 +298,27 @@
X_FRAME_OPTIONS = "SAMEORIGIN"
SILENCED_SYSTEM_CHECKS = ["security.W019"]

### Content Security Policy, via django-csp

# Django 6.0 ships built-in CSP support; drop django-csp and this setting
# and use the framework's own CSP once we upgrade.
# Report-Only first: collect violations and tune the allowlist before
# enforcing. Rollout tracked in #3041.
CONTENT_SECURITY_POLICY_REPORT_ONLY = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test case that checks which HTTP header is emitted? Want to make sure Content-Security-Policy-Report-Only is emitted, not Content-Security-Policy so we don't break everyone on rollout.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test that checks the Report-Only header is set and the enforcing one is not. Runs green.

"DIRECTIVES": {
"default-src": ["'self'"],
"script-src": ["'self'"],
"style-src": ["'self'"],
"img-src": ["'self'", "data:"],
"font-src": ["'self'"],
"connect-src": ["'self'"],
"frame-ancestors": ["'self'"],
"base-uri": ["'self'"],
"object-src": ["'none'"],
"form-action": ["'self'"],
},
}

### django-rest-framework

REST_FRAMEWORK = {
Expand Down
6 changes: 6 additions & 0 deletions pydotorg/tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ def test_admin_caching(self):
self.assertTrue(response.has_header("Cache-Control"))
self.assertEqual(response["Cache-Control"], "private")

def test_csp_report_only_header(self):
"""CSP ships in Report-Only mode; the enforcing header must not be set."""
response = self.client.get("/admin/")
self.assertTrue(response.has_header("Content-Security-Policy-Report-Only"))
self.assertFalse(response.has_header("Content-Security-Policy"))

def test_redirects(self):
"""
More of a sanity check just in case some other middleware interferes.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"dj-database-url==0.5.0",
"django-csp==4.0",
"django-pipeline==4.1.0",
"django-sitetree==1.18.0",
"django-apptemplates==1.5",
Expand Down
15 changes: 15 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.