Django’s
@@ -68,6 +96,7 @@
tab or hitting the back button after a login, you may need to reload the
page with the form, because the token is rotated after a login.
+ {% endif %}
You’re seeing the help section of this page because you have DEBUG =
True
in your Django settings file. Change that to False
,
diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt
index 799f3ee819..1e3fd3e46d 100644
--- a/docs/releases/5.1.txt
+++ b/docs/releases/5.1.txt
@@ -198,6 +198,42 @@ Minor features
session engines now provide async API. The new asynchronous methods all have
``a`` prefixed names, e.g. ``aget()``, ``akeys()``, or ``acycle_key()``.
+:mod:`django.contrib.sitemaps`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.sites`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.staticfiles`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+:mod:`django.contrib.syndication`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* ...
+
+Asynchronous views
+~~~~~~~~~~~~~~~~~~
+
+* ...
+
+Cache
+~~~~~
+
+* ...
+
+CSRF
+~~~~
+
+* The error messaging is improved when ``Origin`` checking fails,
+ including better hints when likely proxy headers are detected.
+
Database backends
~~~~~~~~~~~~~~~~~
diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py
index 956cff11d9..82e40b62ee 100644
--- a/tests/csrf_tests/tests.py
+++ b/tests/csrf_tests/tests.py
@@ -9,7 +9,6 @@ from django.middleware.csrf import (
CSRF_SECRET_LENGTH,
CSRF_SESSION_KEY,
CSRF_TOKEN_LENGTH,
- REASON_BAD_ORIGIN,
REASON_CSRF_TOKEN_MISSING,
REASON_NO_CSRF_COOKIE,
CsrfViewMiddleware,
@@ -891,6 +890,20 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
with self.assertRaises(OSError):
mw.process_view(req, post_form_view, (), {})
+ @override_settings(ALLOWED_HOSTS=["www.example.com"])
+ def test_good_origin_disallowed_host(self):
+ """A request with a disallowed host is rejected."""
+ req = self._get_POST_request_with_token()
+ req.META["HTTP_HOST"] = "www.disallowed.com"
+ req.META["HTTP_ORIGIN"] = "https://www.disallowed.com"
+ mw = CsrfViewMiddleware(post_form_view)
+ self._check_referer_rejects(mw, req)
+ with self.assertLogs("django.security.csrf", "WARNING") as cm:
+ response = mw.process_view(req, post_form_view, (), {})
+ self.assertEqual(response.status_code, 403)
+ msg = "Host is not allowed."
+ self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
+
@override_settings(ALLOWED_HOSTS=["www.example.com"])
def test_bad_origin_bad_domain(self):
"""A request with a bad origin is rejected."""
@@ -899,11 +912,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
req.META["HTTP_ORIGIN"] = "https://www.evil.org"
mw = CsrfViewMiddleware(post_form_view)
self._check_referer_rejects(mw, req)
- self.assertIs(mw._origin_verified(req), False)
+ self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
with self.assertLogs("django.security.csrf", "WARNING") as cm:
response = mw.process_view(req, post_form_view, (), {})
self.assertEqual(response.status_code, 403)
- msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
+ msg = (
+ "Origin checking failed - 'https://www.evil.org' "
+ "does not match 'http://www.example.com' "
+ "or any other trusted origins."
+ )
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
@override_settings(ALLOWED_HOSTS=["www.example.com"])
@@ -914,11 +931,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
req.META["HTTP_ORIGIN"] = "null"
mw = CsrfViewMiddleware(post_form_view)
self._check_referer_rejects(mw, req)
- self.assertIs(mw._origin_verified(req), False)
+ self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
with self.assertLogs("django.security.csrf", "WARNING") as cm:
response = mw.process_view(req, post_form_view, (), {})
self.assertEqual(response.status_code, 403)
- msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
+ msg = (
+ "Origin checking failed - 'null' "
+ "does not match 'http://www.example.com' "
+ "or any other trusted origins."
+ )
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
@override_settings(ALLOWED_HOSTS=["www.example.com"])
@@ -930,11 +951,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
req.META["HTTP_ORIGIN"] = "http://example.com"
mw = CsrfViewMiddleware(post_form_view)
self._check_referer_rejects(mw, req)
- self.assertIs(mw._origin_verified(req), False)
+ self.assertIs(mw._origin_verified(req, "https://www.example.com"), False)
with self.assertLogs("django.security.csrf", "WARNING") as cm:
response = mw.process_view(req, post_form_view, (), {})
self.assertEqual(response.status_code, 403)
- msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
+ msg = (
+ "Origin checking failed - 'http://example.com' "
+ "does not match 'https://www.example.com' "
+ "or any other trusted origins."
+ )
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
@override_settings(
@@ -957,11 +982,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
req.META["HTTP_ORIGIN"] = "http://foo.example.com"
mw = CsrfViewMiddleware(post_form_view)
self._check_referer_rejects(mw, req)
- self.assertIs(mw._origin_verified(req), False)
+ self.assertIs(mw._origin_verified(req, "https://www.example.com"), False)
with self.assertLogs("django.security.csrf", "WARNING") as cm:
response = mw.process_view(req, post_form_view, (), {})
self.assertEqual(response.status_code, 403)
- msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
+ msg = (
+ "Origin checking failed - 'http://foo.example.com' "
+ "does not match 'https://www.example.com' "
+ "or any other trusted origins."
+ )
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
self.assertEqual(mw.allowed_origins_exact, {"http://no-match.com"})
self.assertEqual(
@@ -983,11 +1012,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
req.META["HTTP_ORIGIN"] = "https://["
mw = CsrfViewMiddleware(post_form_view)
self._check_referer_rejects(mw, req)
- self.assertIs(mw._origin_verified(req), False)
+ self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
with self.assertLogs("django.security.csrf", "WARNING") as cm:
response = mw.process_view(req, post_form_view, (), {})
self.assertEqual(response.status_code, 403)
- msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
+ msg = (
+ "Origin checking failed - 'https://[' "
+ "does not match 'http://www.example.com' "
+ "or any other trusted origins."
+ )
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
@override_settings(ALLOWED_HOSTS=["www.example.com"])
@@ -997,7 +1030,7 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
req.META["HTTP_HOST"] = "www.example.com"
req.META["HTTP_ORIGIN"] = "http://www.example.com"
mw = CsrfViewMiddleware(post_form_view)
- self.assertIs(mw._origin_verified(req), True)
+ self.assertIs(mw._origin_verified(req, "http://www.example.com"), True)
response = mw.process_view(req, post_form_view, (), {})
self.assertIsNone(response)
@@ -1009,7 +1042,7 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
req.META["HTTP_HOST"] = "www.example.com"
req.META["HTTP_ORIGIN"] = "https://www.example.com"
mw = CsrfViewMiddleware(post_form_view)
- self.assertIs(mw._origin_verified(req), True)
+ self.assertIs(mw._origin_verified(req, "https://www.example.com"), True)
response = mw.process_view(req, post_form_view, (), {})
self.assertIsNone(response)
@@ -1027,7 +1060,7 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
req.META["HTTP_HOST"] = "www.example.com"
req.META["HTTP_ORIGIN"] = "https://dashboard.example.com"
mw = CsrfViewMiddleware(post_form_view)
- self.assertIs(mw._origin_verified(req), True)
+ self.assertIs(mw._origin_verified(req, "https://www.example.com"), True)
resp = mw.process_view(req, post_form_view, (), {})
self.assertIsNone(resp)
self.assertEqual(mw.allowed_origins_exact, {"https://dashboard.example.com"})
@@ -1047,12 +1080,101 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
req.META["HTTP_HOST"] = "www.example.com"
req.META["HTTP_ORIGIN"] = "https://foo.example.com"
mw = CsrfViewMiddleware(post_form_view)
- self.assertIs(mw._origin_verified(req), True)
+ self.assertIs(mw._origin_verified(req, "https://www.example.com"), True)
response = mw.process_view(req, post_form_view, (), {})
self.assertIsNone(response)
self.assertEqual(mw.allowed_origins_exact, set())
self.assertEqual(mw.allowed_origin_subdomains, {"https": [".example.com"]})
+ @override_settings(ALLOWED_HOSTS=["www.example.com", "localhost"], DEBUG=True)
+ def test_bad_origin_x_forwarded_host_with_port(self):
+ """Give a helpful message if we see a likely X-Forwarded-Host header."""
+ req = self._get_POST_request_with_token()
+ req.META["HTTP_HOST"] = "localhost:8000"
+ req.META["HTTP_ORIGIN"] = "http://www.example.com:8080"
+ req.META["HTTP_X_FORWARDED_HOST"] = "www.example.com:8080"
+ mw = CsrfViewMiddleware(post_form_view)
+ self._check_referer_rejects(mw, req)
+ self.assertIs(mw._origin_verified(req, "http://www.example.com:8000"), False)
+ with self.assertLogs("django.security.csrf", "WARNING") as cm:
+ response = mw.process_view(req, post_form_view, (), {})
+ self.assertEqual(response.status_code, 403)
+ msg = (
+ "Origin checking failed - 'http://www.example.com:8080' "
+ "does not match 'http://localhost:8000' "
+ "or any other trusted origins."
+ )
+ self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
+ self.assertContains(response, "X-Forwarded-Host", status_code=403)
+
+ @override_settings(ALLOWED_HOSTS=["www.example.com", "localhost"], DEBUG=True)
+ def test_bad_origin_x_forwarded_host_no_port(self):
+ """Give a helpful message if we see a likely X-Forwarded-Host header."""
+ req = self._get_POST_request_with_token()
+ req.META["HTTP_HOST"] = "localhost"
+ req.META["HTTP_ORIGIN"] = "http://www.example.com"
+ req.META["HTTP_X_FORWARDED_HOST"] = "www.example.com"
+ mw = CsrfViewMiddleware(post_form_view)
+ self._check_referer_rejects(mw, req)
+ self.assertIs(mw._origin_verified(req, "http://localhost"), False)
+ with self.assertLogs("django.security.csrf", "WARNING") as cm:
+ response = mw.process_view(req, post_form_view, (), {})
+ self.assertEqual(response.status_code, 403)
+ msg = (
+ "Origin checking failed - 'http://www.example.com' "
+ "does not match 'http://localhost' "
+ "or any other trusted origins."
+ )
+ self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
+ self.assertContains(response, "X-Forwarded-Host", status_code=403)
+
+ @override_settings(ALLOWED_HOSTS=["www.example.com"], DEBUG=True)
+ def test_bad_origin_x_forwarded_proto(self):
+ """Give a helpful message if we see a likely X-Forwarded-Proto header."""
+ req = self._get_POST_request_with_token()
+ req.META["HTTP_HOST"] = "www.example.com"
+ req.META["HTTP_ORIGIN"] = "https://www.example.com"
+ req.META["HTTP_X_FORWARDED_PROTO"] = "https"
+ mw = CsrfViewMiddleware(post_form_view)
+ self._check_referer_rejects(mw, req)
+ self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
+ with self.assertLogs("django.security.csrf", "WARNING") as cm:
+ response = mw.process_view(req, post_form_view, (), {})
+ self.assertEqual(response.status_code, 403)
+ msg = (
+ "Origin checking failed - 'https://www.example.com' "
+ "does not match 'http://www.example.com' "
+ "or any other trusted origins."
+ )
+ self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
+ self.assertContains(response, "X-Forwarded-Proto", status_code=403)
+
+ @override_settings(ALLOWED_HOSTS=["www.example.com"], DEBUG=True)
+ def test_no_likely_proxy_headers(self):
+ """Show a message about CSRF_TRUSTED_ORIGINS if no proxy headers."""
+ req = self._get_POST_request_with_token()
+ req.META["HTTP_HOST"] = "www.example.com"
+ req.META["HTTP_ORIGIN"] = "https://www.example.com"
+ mw = CsrfViewMiddleware(post_form_view)
+ self._check_referer_rejects(mw, req)
+ self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
+ with self.assertLogs("django.security.csrf", "WARNING") as cm:
+ response = mw.process_view(req, post_form_view, (), {})
+ self.assertEqual(response.status_code, 403)
+ msg = (
+ "Origin checking failed - 'https://www.example.com' "
+ "does not match 'http://www.example.com' "
+ "or any other trusted origins."
+ )
+ self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
+ self.assertContains(
+ response, "If the expected server origin looks correct", status_code=403
+ )
+ self.assertContains(
+ response, "you may wish to add the origin to", status_code=403
+ )
+ self.assertContains(response, "CSRF_TRUSTED_ORIGINS", status_code=403)
+
class CsrfViewMiddlewareTests(CsrfViewMiddlewareTestMixin, SimpleTestCase):
def _set_csrf_cookie(self, req, cookie):
diff --git a/tests/view_tests/tests/test_csrf.py b/tests/view_tests/tests/test_csrf.py
index 2d530cc586..9d3ecfa84d 100644
--- a/tests/view_tests/tests/test_csrf.py
+++ b/tests/view_tests/tests/test_csrf.py
@@ -130,7 +130,7 @@ class CsrfViewTests(SimpleTestCase):
from django.views.csrf import Path
with mock.patch.object(Path, "open") as m:
- csrf_failure(mock.MagicMock(), mock.Mock())
+ csrf_failure(mock.MagicMock(), "Example reason")
m.assert_called_once_with(encoding="utf-8")
@override_settings(DEBUG=True)