Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #35328 - Improved debug messaging behind proxies. #18014

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ryanhiebert
Copy link
Contributor

Trac ticket number

ticket-35328

Branch description

When in debug mode, we can make some better error messaging when the Origin header checking fails. In common scenarios, we can notice that they are likely missing headers from proxies. In any case, we can remove the verbose messaging around CSRF Tokens, which are not the way they have failed and are unhelpful to solving the error they are viewing.

Checklist

  • This PR targets the main branch.
  • The commit message is written in past tense, mentions the ticket number, and ends with a period.
  • I have checked the "Has patch" ticket flag in the Trac system.
  • I have added or updated relevant tests.
  • I have added or updated relevant docs, including release notes if applicable.

@ryanhiebert ryanhiebert force-pushed the proxies branch 2 times, most recently from 0df060c to 95759af Compare March 25, 2024 02:59
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."
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This ended up requiring a number of test changes. I think that this doesn't run afoul of our backward compatibility policy because I don't think that this is a documented property, but this seems like a likely place for a reviewer to have concerns.

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Yeah, it’s fine to change human-readable error messages because user code shouldn’t generally rely on them. Django’s tests for this code naturally need to check the messages though.

REASON_BAD_ORIGIN
% (
request.META["HTTP_ORIGIN"],
self._host_origin(request) or "<disallowed host>",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would probably be even better to handle this with a dedicated reason, but I didn't want to delay submitting since even with the messaging like this since even as it is it seems like an improvement over the status quo.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I agree this should have it's own reason

@@ -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>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is unrelated to my changes, except that I'm in here and noticed that it wasn't using the same version format for these doc urls.

Copy link
Contributor

Choose a reason for hiding this comment

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

This can be a separate commit, merged independently 👍
Let's pull this out and keep the changeset really tight to the problem being solved

@ryanhiebert ryanhiebert force-pushed the proxies branch 2 times, most recently from a8dadcf to dbbf194 Compare March 25, 2024 16:02
Copy link
Contributor

@sarahboyce sarahboyce left a comment

Choose a reason for hiding this comment

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

Thank you Ryan ⭐ I have initial comments

@@ -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],
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"bad_origin": reason.split(" - ")[0] == REASON_BAD_ORIGIN.split(" - ")[0],
"bad_origin": reason.startswith("Origin checking failed"),

Or maybe

REASON_BAD_ORIGIN_PREFIX = "Origin checking failed"
REASON_BAD_ORIGIN = f"{REASON_BAD_ORIGIN_PREFIX} - %s does not match any trusted origins to server %s."

and

        "bad_origin": reason.startswith(REASON_BAD_ORIGIN_PREFIX),

not sure 🤔

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Just hardcode the string:

Suggested change
"bad_origin": reason.split(" - ")[0] == REASON_BAD_ORIGIN.split(" - ")[0],
"bad_origin": reason.startswith("Origin checking failed"),

It’s easier to read and we have tests that will ensure it’s accurate.

REASON_BAD_ORIGIN
% (
request.META["HTTP_ORIGIN"],
self._host_origin(request) or "<disallowed host>",
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I agree this should have it's own reason

Copy link
Sponsor Member

@adamchainz adamchainz left a comment

Choose a reason for hiding this comment

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

Thank you for your work here @ryanhiebert ! I have some comments with small improvements.

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."
)
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Yeah, it’s fine to change human-readable error messages because user code shouldn’t generally rely on them. Django’s tests for this code naturally need to check the messages though.

Comment on lines +29 to +31
REASON_BAD_ORIGIN = (
"Origin checking failed - %s does not match any trusted origins to server %s."
)
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

The new to server https://example.com suffix is a bit inaccurate, since https://example.com is technically an origin, not a server. How about instead:

Suggested change
REASON_BAD_ORIGIN = (
"Origin checking failed - %s does not match any trusted origins to server %s."
)
REASON_BAD_ORIGIN = (
"Origin checking failed - %r does not match %r or any other trusted origins."
)

Also switching to %r to quote the strings.

@@ -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],
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Just hardcode the string:

Suggested change
"bad_origin": reason.split(" - ")[0] == REASON_BAD_ORIGIN.split(" - ")[0],
"bad_origin": reason.startswith("Origin checking failed"),

It’s easier to read and we have tests that will ensure it’s accurate.

Comment on lines +910 to +913
msg = REASON_BAD_ORIGIN % (
req.META["HTTP_ORIGIN"],
"http://" + req.META["HTTP_HOST"],
)
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

IMO these tests would be better with hardcoded strings - easier to read and debug, and avoiding the chance of incorrect logic.

https://abseil.io/resources/swe-book/html/ch12.html#donapostrophet_put_logic_in_tests

Comment on lines +54 to +56
<code>{{x_forwarded_proto}}</code></li>
<li><strong><code>X-Forwarded-Host</code></strong>:
<code>{{x_forwarded_host}}</code></li>
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Suggested change
<code>{{x_forwarded_proto}}</code></li>
<li><strong><code>X-Forwarded-Host</code></strong>:
<code>{{x_forwarded_host}}</code></li>
<code>{{ x_forwarded_proto }}</code></li>
<li><strong><code>X-Forwarded-Host</code></strong>:
<code>{{ x_forwarded_host }}</code></li>

https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#template-style

</pre></p>
{% else %}
If the expected server origin looks correct,
you may wish to add the origin to the CSRF_TRUSTED_ORIGINS setting.
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Suggested change
you may wish to add the origin to the CSRF_TRUSTED_ORIGINS setting.
you may wish to add the origin to the <code>CSRF_TRUSTED_ORIGINS</code>
setting.

@@ -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):
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Could we restructure the code to avoid calling _host_origin twice (within _origin_verified() and then on line 451). So here could look like:

Suggested change
if not self._origin_verified(request):
host_origin = self._host_origin(request)
if not self._origin_verified(request, host_origin):

And the error message could use host_origin or "<disallowed host>".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants