Skip to content

Commit bf13b2c

Browse files
committed
Implement security.txt feedback
- Serve from a view, nginx is not going to handle this for us. - Make clear what should be reported to the [email protected] and what should be reported to the website working group - Downgrade security.txt expiration test to a warning instead of a hard fail.
1 parent ad8da62 commit bf13b2c

File tree

6 files changed

+114
-40
lines changed

6 files changed

+114
-40
lines changed

.well-known/security.txt

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{% spaceless %}
2+
{% comment %}
3+
This file is served under the well-known URIs
4+
5+
- https://www.djangoproject.com/.well-known/security.txt
6+
- https://docs.djangoproject.com/.well-known/security.txt
7+
8+
See https://securitytxt.org/ for more information about the security.txt standard.
9+
{% endcomment %}
10+
{% endspaceless %}# Hello security researcher!
11+
# We appreciate your help in keeping Django & djangoproject.com secure.
12+
13+
# Please report security issues that concern this website (djangoproject.com)
14+
# to the website working group: [email protected]
15+
# This helps us make sure your report is directed to the right people.
16+
# You can find guidelines for reporting website security issues here: https://github.com/django/djangoproject.com/blob/main/.github/SECURITY.md
17+
18+
# DO NOT USE [email protected] FOR ISSUES THAT CONCERN THE WEBSITE.
19+
20+
# If your report concerns Django itself (the Python package, not this website), please follow the Django security reporting process:
21+
Policy: https://www.djangoproject.com/security/
22+
Contact: https://www.djangoproject.com/security/
23+
Expires: 2026-12-31T00:00:00.000Z
24+
Preferred-Languages: en
25+
26+
# If you would like to encrypt your report, you can use the following PGP key:
27+
Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/AF3516D27D0621171E0CCE25FCB84B8D1D17F80B

djangoproject/tests.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import re
2+
import warnings
13
from datetime import datetime, timedelta
24
from http import HTTPStatus
35
from io import StringIO
46

5-
from django.conf import settings
67
from django.core.management import call_command
78
from django.test import TestCase
89
from django.urls import NoReverseMatch, get_resolver
@@ -168,38 +169,37 @@ def test_single_h1_per_page(self):
168169
self.assertContains(response, "<h1", count=1)
169170

170171

171-
class SecurityTxtFileTests(TestCase):
172+
class SecurityTxtTests(TestCase):
172173
"""
173174
Tests for the security.txt file.
174175
"""
175176

176-
def test_security_txt_not_expired(self):
177+
def test_security_txt(self):
177178
"""
178-
The security.txt file should not be expired.
179+
The security.txt file should be reachable at the expected URL.
179180
"""
180-
FILE_PATH = settings.BASE_DIR / ".well-known" / "security.txt"
181-
with open(FILE_PATH) as f:
182-
content = f.read()
183-
# Read the line that starts with "Expires:", and parse the date.
184-
for line in content.splitlines():
185-
if line.startswith("Expires:"):
186-
expires = line.strip("Expires: ")
187-
break
188-
else:
189-
self.fail("No Expires line found in security.txt")
190-
191-
expires_date = datetime.strptime(
192-
expires,
193-
"%Y-%m-%dT%H:%M:%S.%fZ",
194-
).date()
195-
# We should ideally be two weeks early with updating - active over reactive
196-
cutoff = (datetime.now() - timedelta(days=15)).date()
197-
self.assertGreater(
198-
expires_date,
199-
cutoff,
200-
"The security.txt file is close to expiring. \
201-
Please update the 'Expires' line in to confirm the contents are \
202-
still accurate: {}".format(
203-
FILE_PATH
204-
),
181+
response = self.client.get("/.well-known/security.txt")
182+
self.assertEqual(response.status_code, HTTPStatus.OK)
183+
self.assertEqual(response["Content-Type"], "text/plain")
184+
self.assertTrue("public" in response["Cache-Control"])
185+
186+
match = re.search(
187+
"^Expires: (.*)$", response.content.decode("utf-8"), flags=re.MULTILINE
188+
)
189+
if match is None:
190+
self.fail("No Expires line found in security.txt")
191+
else:
192+
expires = match[1]
193+
194+
expires_date = datetime.strptime(
195+
expires,
196+
"%Y-%m-%dT%H:%M:%S.%fZ",
197+
).date()
198+
199+
if expires_date < datetime.now().date() - timedelta(days=15):
200+
warnings.warn(
201+
"The djangoproject/templates/well-known/security.txt file is"
202+
" close to expiring. Please update the 'Expires' line to confirm"
203+
" the contents are still accurate.",
204+
category=UserWarning,
205205
)

djangoproject/urls/docs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.contrib.sitemaps.views import sitemap
44
from django.http import HttpResponse
55
from django.urls import include, path
6+
from django.views.decorators.cache import cache_control
7+
from django.views.generic import TemplateView
68

79
from docs.models import DocumentRelease
810
from docs.sitemaps import DocsSitemap
@@ -55,6 +57,14 @@ def __setitem__(key, value):
5557
"google-site-verification: google79eabba6bf6fd6d3.html"
5658
),
5759
),
60+
path(
61+
".well-known/security.txt",
62+
cache_control(max_age=60 * 60 * 24, public=True)(
63+
TemplateView.as_view(
64+
template_name="well-known/security.txt", content_type="text/plain"
65+
)
66+
),
67+
),
5868
# This just exists to make sure we can proof that the error pages work
5969
# under both hostnames.
6070
path("", include("legacy.urls")),

djangoproject/urls/www.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.contrib.flatpages.sitemaps import FlatPageSitemap
66
from django.contrib.sitemaps import views as sitemap_views
77
from django.urls import include, path, re_path
8-
from django.views.decorators.cache import cache_page
8+
from django.views.decorators.cache import cache_control, cache_page
99
from django.views.generic import RedirectView, TemplateView
1010
from django.views.static import serve
1111

@@ -136,6 +136,14 @@
136136
cache_page(60 * 60 * 6)(sitemap_views.sitemap),
137137
{"sitemaps": sitemaps},
138138
),
139+
path(
140+
".well-known/security.txt",
141+
cache_control(max_age=60 * 60 * 24, public=True)(
142+
TemplateView.as_view(
143+
template_name="well-known/security.txt", content_type="text/plain"
144+
)
145+
),
146+
),
139147
path("weblog/", include("blog.urls")),
140148
path("download/", include("releases.urls")),
141149
path("svntogit/", include("svntogit.urls")),

docs/tests/test_views.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import re
2+
import warnings
3+
from datetime import datetime, timedelta
14
from http import HTTPStatus
25

36
from django.contrib.sites.models import Site
@@ -268,3 +271,39 @@ def test_sitemap_404(self):
268271
self.assertEqual(
269272
response.context["exception"], "No sitemap available for section: 'xx'"
270273
)
274+
275+
276+
class SecurityTxtTests(TestCase):
277+
"""
278+
Tests for the security.txt file.
279+
"""
280+
281+
def test_security_txt(self):
282+
"""
283+
The security.txt file should be reachable at the expected URL.
284+
"""
285+
response = self.client.get("/.well-known/security.txt")
286+
self.assertEqual(response.status_code, HTTPStatus.OK)
287+
self.assertEqual(response["Content-Type"], "text/plain")
288+
self.assertTrue("public" in response["Cache-Control"])
289+
290+
match = re.search(
291+
"^Expires: (.*)$", response.content.decode("utf-8"), flags=re.MULTILINE
292+
)
293+
if match is None:
294+
self.fail("No Expires line found in security.txt")
295+
else:
296+
expires = match[1]
297+
298+
expires_date = datetime.strptime(
299+
expires,
300+
"%Y-%m-%dT%H:%M:%S.%fZ",
301+
).date()
302+
303+
if expires_date < datetime.now().date() - timedelta(days=15):
304+
warnings.warn(
305+
"The djangoproject/templates/well-known/security.txt file is"
306+
" close to expiring. Please update the 'Expires' line to confirm"
307+
" the contents are still accurate.",
308+
category=UserWarning,
309+
)

0 commit comments

Comments
 (0)