Skip to content

Commit

Permalink
Fixed #35328 - Improved debug messaging behind proxies.
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanhiebert committed Mar 25, 2024
1 parent b6e2b83 commit 95759af
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 16 deletions.
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Django",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/../csrf/manage.py",
"args": ["runserver"],
"django": true,
"justMyCode": false
},
]
}
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@ answer newbie questions, and generally made Django that much better:
Russ Webber
Ryan Hall <ryanhall989@gmail.com>
Ryan Heard <ryanwheard@gmail.com>
Ryan Hiebert <ryan@ryanhiebert.com>
ryankanno
Ryan Kelly <ryan@rfk.id.au>
Ryan Niemeyer <https://profiles.google.com/ryan.niemeyer/about>
Expand Down
11 changes: 11 additions & 0 deletions before-submitting.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Before submitting
=================

* Backward compatibility concern for the message.
But the constant, and therefore the ability to use it
with a given number of arguments, is not documented,
so is not under the backward compatibility policy.
* Determine what tests need to be written for the CSRF template and view.
* Determine what documentation needs to be written about this change.
* doesn't need any feature documentation
* could have documentation in release notes, but seems too small. Are tiny changes documented?
29 changes: 21 additions & 8 deletions django/middleware/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
# This matches if any character is not in CSRF_ALLOWED_CHARS.
invalid_token_chars_re = _lazy_re_compile("[^a-zA-Z0-9]")

REASON_BAD_ORIGIN = "Origin checking failed - %s does not match any trusted origins."
REASON_BAD_ORIGIN = (
"Origin checking failed - %s does not match any trusted origins to server %s."
)
REASON_NO_REFERER = "Referer checking failed - no Referer."
REASON_BAD_REFERER = "Referer checking failed - %s does not match any trusted origins."
REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
Expand Down Expand Up @@ -268,19 +270,25 @@ def _set_csrf_cookie(self, request, response):
# Set the Vary header since content varies with the CSRF cookie.
patch_vary_headers(response, ("Cookie",))

def _origin_verified(self, request):
request_origin = request.META["HTTP_ORIGIN"]
def _host_origin(self, request):
"""
Find the host origin that is automatically trusted.
"""
try:
good_host = request.get_host()
except DisallowedHost:
pass
return
else:
good_origin = "%s://%s" % (
return "%s://%s" % (
"https" if request.is_secure() else "http",
good_host,
)
if request_origin == good_origin:
return True

def _origin_verified(self, request):
request_origin = request.META["HTTP_ORIGIN"]
host_origin = self._host_origin(request)
if host_origin and request_origin == host_origin:
return True
if request_origin in self.allowed_origins_exact:
return True
try:
Expand Down Expand Up @@ -436,7 +444,12 @@ def process_view(self, request, callback, callback_args, callback_kwargs):
if "HTTP_ORIGIN" in request.META:
if not self._origin_verified(request):
return self._reject(
request, REASON_BAD_ORIGIN % request.META["HTTP_ORIGIN"]
request,
REASON_BAD_ORIGIN
% (
request.META["HTTP_ORIGIN"],
self._host_origin(request) or "<disallowed host>",
),
)
elif request.is_secure():
# If the Origin header wasn't provided, reject HTTPS requests if
Expand Down
16 changes: 15 additions & 1 deletion django/views/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ def csrf_failure(request, reason="", template_name=CSRF_FAILURE_TEMPLATE_NAME):
"""
Default view used when request fails CSRF protection
"""
from django.middleware.csrf import REASON_NO_CSRF_COOKIE, REASON_NO_REFERER
from django.middleware.csrf import (
REASON_BAD_ORIGIN,
REASON_NO_CSRF_COOKIE,
REASON_NO_REFERER,
)

c = {
"title": _("Forbidden"),
Expand Down Expand Up @@ -61,6 +65,16 @@ def csrf_failure(request, reason="", template_name=CSRF_FAILURE_TEMPLATE_NAME):
"re-enable them, at least for this site, or for “same-origin” "
"requests."
),
"bad_origin": reason.split(" - ")[0] == REASON_BAD_ORIGIN.split(" - ")[0],
"forwarded_may_fix": (
request.headers.get("X-Forwarded-Proto", "") == "https"
and not request.is_secure()
or request.headers.get("Origin", "").endswith(
f'://{request.headers.get("X-Forwarded-Host", "")}'
)
),
"x_forwarded_proto": request.headers.get("X-Forwarded-Proto"),
"x_forwarded_host": request.headers.get("X-Forwarded-Host"),
"DEBUG": settings.DEBUG,
"docs_version": get_docs_version(),
"more": _("More information is available with DEBUG=True."),
Expand Down
29 changes: 28 additions & 1 deletion django/views/templates/csrf_403.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ <h2>Help</h2>
</pre>
{% endif %}

{% if bad_origin %}
{% if forwarded_may_fix %}
<p>The <code>Origin</code> header does not match
the expected server origin,
but common proxy headers are present in the request
and may include parts of the <code>Origin</code> header.</p>
<ul>
<li><strong><code>X-Forwarded-Proto</code></strong>:
<code>{{x_forwarded_proto}}</code></li>
<li><strong><code>X-Forwarded-Host</code></strong>:
<code>{{x_forwarded_host}}</code></li>
</ul>
<p>After you understand the
<a href="https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#secure-proxy-ssl-header">important security considerations</a>,
you may wish to add one or more of the following
settings to permit Django to trust these headers.</p>
<p><pre>
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
</pre></p>
{% else %}
If the expected server origin looks correct,
you may wish to add the origin to the CSRF_TRUSTED_ORIGINS setting.
{% endif %}
{% else %}

<p>In general, this can occur when there is a genuine Cross Site Request Forgery, or when
<a
href="https://docs.djangoproject.com/en/{{ docs_version }}/ref/csrf/">Django’s
Expand All @@ -53,7 +79,7 @@ <h2>Help</h2>
<li>Your browser is accepting cookies.</li>

<li>The view function passes a <code>request</code> to the template’s <a
href="https://docs.djangoproject.com/en/dev/topics/templates/#django.template.backends.base.Template.render"><code>render</code></a>
href="https://docs.djangoproject.com/en/{{ docs_version }}/topics/templates/#django.template.backends.base.Template.render"><code>render</code></a>
method.</li>

<li>In the template, there is a <code>{% templatetag openblock %} csrf_token
Expand All @@ -68,6 +94,7 @@ <h2>Help</h2>
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.</li>
</ul>
{% endif %}

<p>You’re seeing the help section of this page because you have <code>DEBUG =
True</code> in your Django settings file. Change that to <code>False</code>,
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/5.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ Cache
CSRF
~~~~

* ...
* The error messaging is improved when ``Origin`` checking fails,
including better hints when likely proxy headers are detected.

Database backends
~~~~~~~~~~~~~~~~~
Expand Down
186 changes: 186 additions & 0 deletions results-of-runtests
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
======================================================================
ERROR: test_bad_origin_bad_domain (csrf_tests.tests.CsrfViewMiddlewareTests)
A request with a bad origin is rejected.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 910, in test_bad_origin_bad_domain
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_bad_origin_bad_protocol (csrf_tests.tests.CsrfViewMiddlewareTests)
A request with an origin with wrong protocol is rejected.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 941, in test_bad_origin_bad_protocol
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_bad_origin_cannot_be_parsed (csrf_tests.tests.CsrfViewMiddlewareTests)
A POST request with an origin that can't be parsed by urlparse() is
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 994, in test_bad_origin_cannot_be_parsed
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_bad_origin_csrf_trusted_origin_bad_protocol (csrf_tests.tests.CsrfViewMiddlewareTests)
A request with an origin with the wrong protocol compared to
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 968, in test_bad_origin_csrf_trusted_origin_bad_protocol
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_bad_origin_null_origin (csrf_tests.tests.CsrfViewMiddlewareTests)
A request with a null origin is rejected.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 925, in test_bad_origin_null_origin
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_bad_origin_bad_domain (csrf_tests.tests.CsrfViewMiddlewareUseSessionsTests)
A request with a bad origin is rejected.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 910, in test_bad_origin_bad_domain
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_bad_origin_bad_protocol (csrf_tests.tests.CsrfViewMiddlewareUseSessionsTests)
A request with an origin with wrong protocol is rejected.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 941, in test_bad_origin_bad_protocol
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_bad_origin_cannot_be_parsed (csrf_tests.tests.CsrfViewMiddlewareUseSessionsTests)
A POST request with an origin that can't be parsed by urlparse() is
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 994, in test_bad_origin_cannot_be_parsed
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_bad_origin_csrf_trusted_origin_bad_protocol (csrf_tests.tests.CsrfViewMiddlewareUseSessionsTests)
A request with an origin with the wrong protocol compared to
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 968, in test_bad_origin_csrf_trusted_origin_bad_protocol
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_bad_origin_null_origin (csrf_tests.tests.CsrfViewMiddlewareUseSessionsTests)
A request with a null origin is rejected.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/django/test/utils.py", line 446, in inner
return func(*args, **kwargs)
File "/workspaces/django/tests/csrf_tests/tests.py", line 925, in test_bad_origin_null_origin
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
TypeError: not enough arguments for format string

======================================================================
ERROR: test_template_encoding (view_tests.tests.test_csrf.CsrfViewTests)
The template is loaded directly, not via a template loader, and should
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 59, in testPartExecutor
yield
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 591, in run
self._callTestMethod(testMethod)
File "/usr/local/python/3.10.13/lib/python3.10/unittest/case.py", line 549, in _callTestMethod
method()
File "/workspaces/django/tests/view_tests/tests/test_csrf.py", line 133, in test_template_encoding
csrf_failure(mock.MagicMock(), mock.Mock())
File "/workspaces/django/django/views/csrf.py", line 68, in csrf_failure
"bad_origin": reason.split(" - ")[0] == REASON_BAD_ORIGIN.split(" - ")[0],
TypeError: 'Mock' object is not subscriptable

0 comments on commit 95759af

Please sign in to comment.