Skip to content

Commit 318757e

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 318757e

File tree

6 files changed

+100
-39
lines changed

6 files changed

+100
-39
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: 25 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,33 @@ 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+
185+
match = re.search(
186+
"^Expires: (.*)$", response.content.decode("utf-8"), flags=re.MULTILINE
187+
)
188+
if match is None:
189+
self.fail("No Expires line found in security.txt")
190+
else:
191+
expires = match[1]
192+
193+
expires_date = datetime.fromisoformat(expires).date()
194+
195+
if expires_date < datetime.now().date() - timedelta(days=15):
196+
warnings.warn(
197+
"The djangoproject/templates/well-known/security.txt file is"
198+
" close to expiring. Please update the 'Expires' line to confirm"
199+
" the contents are still accurate.",
200+
category=UserWarning,
205201
)

djangoproject/urls/docs.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
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.generic import TemplateView
67

78
from docs.models import DocumentRelease
89
from docs.sitemaps import DocsSitemap
@@ -55,6 +56,12 @@ def __setitem__(key, value):
5556
"google-site-verification: google79eabba6bf6fd6d3.html"
5657
),
5758
),
59+
path(
60+
".well-known/security.txt",
61+
TemplateView.as_view(
62+
template_name="well-known/security.txt", content_type="text/plain"
63+
),
64+
),
5865
# This just exists to make sure we can proof that the error pages work
5966
# under both hostnames.
6067
path("", include("legacy.urls")),

djangoproject/urls/www.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@
136136
cache_page(60 * 60 * 6)(sitemap_views.sitemap),
137137
{"sitemaps": sitemaps},
138138
),
139+
path(
140+
".well-known/security.txt",
141+
TemplateView.as_view(
142+
template_name="well-known/security.txt", content_type="text/plain"
143+
),
144+
),
139145
path("weblog/", include("blog.urls")),
140146
path("download/", include("releases.urls")),
141147
path("svntogit/", include("svntogit.urls")),

docs/tests/test_views.py

Lines changed: 35 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,35 @@ 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+
289+
match = re.search(
290+
"^Expires: (.*)$", response.content.decode("utf-8"), flags=re.MULTILINE
291+
)
292+
if match is None:
293+
self.fail("No Expires line found in security.txt")
294+
else:
295+
expires = match[1]
296+
297+
expires_date = datetime.fromisoformat(expires).date()
298+
299+
if expires_date < datetime.now().date() - timedelta(days=15):
300+
warnings.warn(
301+
"The djangoproject/templates/well-known/security.txt file is"
302+
" close to expiring. Please update the 'Expires' line to confirm"
303+
" the contents are still accurate.",
304+
category=UserWarning,
305+
)

0 commit comments

Comments
 (0)