Why is the use of `url_has_allowed_host_and_scheme` discouraged

Hello Djangonauts.

First time poster here :wave:.

I have been looking for a way to check that a redirect URL is “safe” (e.g. local to the current site). I have hand rolled a function to only leave the path without the domain. Then I thought this must be a common problem, there must be something in Django.

Turns out there was is_safe_url, which was renamed to url_has_allowed_host_and_scheme. However, both are undocumented and only mentioned in the release notes. And when they are mentioned it does not sound like you are supposed to be using them.

Why is that?

Then there is this quote from the 4.2 release notes:

This is to protect projects that may be incorrectly using the internal url_has_allowed_host_and_scheme() function, instead of using one of the documented functions for handling URL redirects.

Django 4.2 release notes | Django documentation | Django

The most interesting part is “instead of using one of the documented functions for handling URL redirects”. I tried but failed to find the documented functions for handling URL redirects. Does anyone have any pointers what those might be?

url_has_allowed_host_and_scheme is an internal function because it only does part of what’s needed to safely use a provided URL in a redirect. There’s escaping issues and other considerations, all of which need to be applied, and for which it’s easy to miss.

Before the change you link the function was called is_safe_url — which gave folks entirely the wrong impression: they’d use that — despite the warnings that say don’t — and think they’d done enough.

The release note is only there as a courtesy to those folks.

Again this is an internal function. Don’t use it.

If you want to do redirects you should always use the documented HttpResponseRedirect which correctly handles the various issues here for you.

Thanks for your response @carltongibson.

I am not sure I understand how HttpResponseRedirect will make sure that the redirect is safe. Wouldn’t it happily redirect to an unsafe site if one used something like HttpReponseRedirect(url=request.GET["next"]) (ignoring that the key could be missing of course)?

I agree, the rename to url_has_allowed_host_and_scheme makes it much clearer of what the function does. If a URL that passes those checks can be considered safe would depend on the given context.

I understand that the function is meant for internal use and not part of the documented public API of Django.

What I don’t quite understand is: why?

It seems like a perfectly valid utility function that does what it says.

It is used exactly how you would expect in the django.auth.views module to check the “next” parameter.

What I don’t understand is what would make using the function elsewhere in the exact same manner (to check a URL coming from a query parameter) so worrisome?

EDIT: I guess my question is: Why is it considered “private and internal”?

The code you quote there goes on to use the redirect URL with an HttpResponseRedirect. The latter then performs required escaping before using the URL. It’s only in such combination that a URL is safe.

Django doesn’t provide equivalent utilities as part of the public API because doing so in a generic way but such that folks won’t step into security problems has not been considered feasible.

Thanks for expanding on this @carltongibson.

The URL escaping you mention, is that the typical % escaping of potential query parameters in the URL which was checked to be of allowed host and scheme? We would not want to escape the whole URL right, because then it wouldn’t work as a URL?

Similar to what’s mentioned on this cheat sheet?

Ah, OK. You want the full details. That will require me to go over the history. I’m happy to do that on the back burner, for my own refresh, but it won’t be instant I’m afraid.

I would definitely be happy about anything that helps me understand this fully.


Sorry for the late response I did not get a notification for some reason.

OK, no problem. I’ll see if I can write something up. (No deadline! :sweat_smile:)

tl;dr will be ≈"folks still need to correctly escape the URL, and that’s non-obvious"

Thanks @carltongibson. That would be much appreciated.