Improving Heroku's Suggestions for Django

Context
I reached out to Heroku Support with a question related to the SSL not working after setting it up and, in addition to answering my actual question, they added this unsolicited recommendation:

Furthermore, If you wish to force route http:// requests to https://, you can do it at application level as Heroku router doesn’t provide this functionailty. You may check out the instructions provided here: https://help.heroku.com/J2R1S4T8/can-heroku-force-an-application-to-use-ssl-tls .

In the article they linked, it recommends the following for Django:

Add the following to your Settings file, adding the middleware to your existing MIDDLEWARE list if one exists.

MIDDLEWARE = [
    # SecurityMiddleware must be listed before other middleware
    'django.middleware.security.SecurityMiddleware',
    # ...
]

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True

This didn’t work for me and so I went to the SECURE_PROXY_SSL_HEADER section in the Django docs, and I saw the following huge warning:

Warning

Modifying this setting can compromise your site’s security. Ensure you fully understand your setup before changing it.

Make sure ALL of the following are true before setting this (assuming the values from the example above):

  • Your Django app is behind a proxy.
  • Your proxy strips the X-Forwarded-Proto header from all incoming requests, even when it contains a comma-separated list of protocols. In other words, if end users include that header in their requests, the proxy will discard it.
  • Your proxy sets the X-Forwarded-Proto header and sends it to Django, but only for requests that originally come in via HTTPS.

If any of those are not true, you should keep this setting set to None and find another way of determining HTTPS, perhaps via custom middleware.

After reading a few Stackoverflow articles that I didn’t really understand (like this one, for example), most of them noted (among other things) removing the SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') setting that Heruko recommended in their article.

In this Django Best Practices: Security article, Will doesn’t mention editing SECURE_PROXY_SSL_HEADER and since I trust his judgement and don’t understand enough yet to do anything but follow a trusted source, I did what was outlined in his article and left out the SECURE_PROXY_SSL_HEADER recommendation from Heroku.

Question
All of that context just to say that I think it would benefit the Django community if Heroku updates the Django section of their article. I’m happy to help by reaching out to Heroku directly to see if they’d be willing to update it, however, as a newcomer, I’m unsure what the recommendation should actually be and don’t really want to speak on behalf of Django without some sort of assurance that what I’d suggest they change is actually better for the typical Django user deploying with Heroku than what is already there.

At minimum, I want to recommend that they include links to the relevant Django documentation sections (SECURE_PROXY_SSL_HEADER and SECURE_SSL_REDIRECT).

More ideally, it feels like the default suggestion they recommend might be better if updated to not edit SECURE_PROXY_SSL_HEADER (but I really don’t know - is that true? What should be the default that Heroku’s article covers in their Django section of their article?)

Let’s start by answering the basic question, what does SECURE_PROXY_SSL_HEADER do?

Take a step back for a moment. We might think that a browser is connecting directly to your project:

[ browser ] ---> https ---> [ Django project ]

However, in the case of a “production-quality” deployment, when you’re using a web server, the connection is actually with the web server. That web server then forwards the request to the Django project:

[ browser ] ---> https ---> [ web server ] ---> ??? ---> [ Django project ]

In the “common case”, that second connection (identified by ???) is going to be http and not https.

What does this mean? It means that Django is going to think that this request is not secured by https, which can and will cause a number of problems in different areas. (If nothing else, it’s likely to cause URLs being generated by the static tag to generate http://... urls and not https://... urls.)

How do we fix this? How do we tell Django that the browser is actually using https?

That is the purpose of the SECURE_PROXY_SSL_HEADER setting.

When you set:

You’re telling Django, to check this header (HTTP_X_FORWARDED_PROTO) which is generated by the web server (not coming from the browser) for the value ‘https’. If that header has that value, then Django knows to treat that request as if it were requested by the browser as https://....

And that’s why the docs have that warning. Django is trusting that the web server is doing the right thing, and if it isn’t, you may have a real problem.

Having said that, this is exactly what we do. We use nginx as the SSL endpoint, and proxy all requests to the backends as http - but telling those endpoints (via that header) that the original requests were all https.

2 Likes