Do we need an easier toggle for `request.is_secure()` to be True?

Hi all.

HttpRequest.is_secure(): Returns True if the request is secure; that is, if it was made with HTTPS.

Django isn’t magic here: it goes on a couple of clues as to what to answer there:

Getting this right has always been a bit of a pain.

Since Django 4.0 though, CSRF protection uses this to check the Origin header, and there are lots of posts of people tripping up on it.

But there are legion. A search for CSRF trusted origins has the same issue again and again and again.

I think :thinking: the main issue is that SECURE_PROXY_SSL_HEADER is hard to get right, and varies between environments significantly.

But, in contrast to the old days, HTTPS is the norm now. Having a really fiddly setting, dependent on equally fiddly settings in my hosting environment, just to get the default to work seems (maybe) an undue burden now.

Should we add some toggle — Grrr, a setting? :grimacing: — to more easily say, This project will always be served with HTTPS, and have HttpRequest.is_secure() always defer to that?

This is security sensitive, so we must be careful not just to jump on Yes. :rotating_light:

While I understand that people are having an issue with this, it is worth noting that the combination of SECURE_PROXY_SSL_HEADER and wsgi.url_scheme always fixes this. I do give you that it requires an understanding of how http headers are used to communicate between proxies and the wsgi server, but that is not the worst thing for something security sensitive.

Your TIL is interesting for multiple reasons: First off, it obviously fixes the problem for you by tying directly into the WSGI env variables. It also has the upside that for instance Gunicorn can use this information for logging (I am doing similar things with the remote ip, which Gunicorn would otherwise only see as the proxy in front of it). So while your approach does have the upside of making this work with everything being able to work of WSGI env variables (be that gunicorn or for instance a sentry and other tools that tap into that info) it also has the downside of being code and hard to adjust to different environments if it extends to more than just HTTPS handling (though I agree that is not your goal here).

So all in all I am somewhere between +0 and -0 personally – I can see this being a pain for people, but I think that the current code works well enough for this. On the other hand (and I know how to configure this stuff by heart) I am also often in the situation where I know that I have a proxy in the same network namespace and the only way to talk to my app is secured via HTTPS, and I really don’t want to waste time remembering which header my proxy uses this time.

This whole thing also reminds me a bit of https://groups.google.com/g/django-developers/c/YzRj7OXpLkk (thanks to @sarahboyce for putting that up – took me a while to find it since I was searching for it in the forum :D). While a BASE_URL wouldn’t tell you whether you are actually on HTTP or HTTPS (it solely is ment to provide a “default”) it also taps into two things that are kinda “hard” to get. One is the domain you are currently on (depending on the proxy this might or might not be the normal Host header but an X-Forwarded-Host header) and the other is the subpath that Django is mounted on.

Hope that helps :slight_smile:

1 Like

I’m 100% on board with Carlton’s line of thought here. +1 on just directly exposing this in a setting.

I think the other big thing from that TIL is that Django’s default CSRF Middleware doesn’t expose enough information to aid in debugging. Which is why recommendations for “just set CSRF_TRUSTED_ORIGINS” are plastered across the internet.

My own solution for that one is GitHub - bugsink/verbose_csrf_middleware: Django CSRF middleware, but more verbose in its failures.
Arguably, such a solution should (could) be part of Django, but given the security tradeoffs I haven’t bothered to push for that yet.

@CodenameTim posted a similar point about the middleware over here:

You might want to also join the discussion there @vanschelven. (I think there’s a general vibe in support of something there, so it’s bandwidth to get it over the line…)

There is one use case that SECURE_PROXY_SSL_HEADER and wsgi.url_scheme is not enough to determine if user is visiting HTTPS. That is when Django is behind two layers of reverse proxies:

       https                 http           http
User ----------> CloudFlare -------> Nginx -------> Django

In this case, even though CloudFlare sets correct X-Forwarded-Proto: https, Nginx will overwrite it to http and make Django think that user is visiting plain HTTP.

I would think in this situation that you could use the proxy_pass_header configuration parameter to pass the X-Forwarded-Proto from what is received in the request being proxied by CloudFlare.

Yes, I’m using this method, but I feel that it is just a workaround.

Hi @quan,

My take here is that Configure your web server properly is the (strictly) correct answer.

But I think it’s too much of a lift — it’s too esoteric/opaque — to be the default we go with.

I think I would point folks generally to either of the WSGI or Django Middleware approaches, currently, but I (still) think those are a little too advanced/hard given that ≈everybody ≈always uses HTTPS these days.

I’m not generally a big fan of settings, but I think a single toggle here, used by HttpRequest.is_secure() as the first path, would be a good usability addition.

(I vividly recall the first time I had to work this out myself. It must have taken two days to get right.)

Edit: Actually, glancing again at the Django Middleware example (linked above), if Django shipped that (or better) and the fix here was just to add a single line to MIDDLEWARE, I think that would be fine too.

Another Edit: @apollo13 points out that a middleware might just as well adjust the WSGI environment (or ASGI scope, and request), which would (of course) work too, and would have benefits for logging and such. If we go this route we can investigate.

Thanks @carltongibson and @KenWhitesell

I didn’t read Ken carefully. At a glance, I misread that he proposed to use proxy_set_header and thought that it was workaround (that was what I did also). But now I read again, proxy_pass_header is a correct solution.