70

The EFF recommends using HTTPS everywhere on your site, and I'm sure this site would agree. When I asked a question about using Django to implement HTTPS on my login page, that was certainly the response I got :)

So I'm trying to do just that. I have a Django/nginx setup that I'm trying to configure for HTTPS-only - it's sort of working, but there are problems. More importantly, I'm sure if it's really secure, despite seeing the https prefix.

I have configured nginx to redirect all http pages to https, and that part works. However... Say I have a page, https://mysite.com/search/, with a search form/button on it. I click the button, Django processes the form, and does a redirect to a results page, which is http://mysite.com/search/results?term="foo".

This URL gets sent to the browser, which sends it back to the nginx server, which does a permanent redirect to an https-prefixed version of the page. (At least I think that's what is happening - certainly IE warns me that I'm going to an insecure page, and then right back to a secure page :)

But is this really secure? Or, at least as much security as a standard HTTPS-only site would have? Is the fact that Django transmits a http-prefix URL, someone compromising security? Yes, as far as I can tell, only pages that have an https-prefix get replied to, but it just doesn't feel right :) Security is funky, as this site can attest to, and I'm worried there's something I'm missing.

4
  • How do you have the form's action attribute setup? If you force it to the full https url does that fix it?
    – mikeazo
    Commented Nov 16, 2011 at 21:03
  • @mikeazo, the HTML syntax I use is ` <form action="." method="post" id="search">{% csrf_token %} `. The actual redirection is done in the Python view code, and the URL is determined by Django.
    – John C
    Commented Nov 16, 2011 at 21:26
  • 3
    do not serve any page on HTTP except for a page which either redirects to your secure site or displays a message with link to your secure site. And Only HTTPS is not equal to a secure site, HTTPS only insures that your traffic is encrypted between your browser and server nothing more. So do not ignore other things and build security into your application Commented Nov 17, 2011 at 6:26
  • @JohnC fixed (njinx) typo for nginx.
    – dr jimbob
    Commented Nov 17, 2011 at 14:55

7 Answers 7

75

Secure your cookies

In settings.py put the lines

SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

and cookies will only be sent via HTTPS connections. Additionally, you probably also want SESSION_EXPIRE_AT_BROWSER_CLOSE=True. Note if you are using older versions of django (less than 1.4), there isn't a setting for secure CSRF cookies. As a quick fix, you can just have CSRF cookie be secure when the session cookie is secure (SESSION_COOKIE_SECURE=True), by editing django/middleware/csrf.py:

class CsrfViewMiddleware(object):
   ...
   def process_response(self, request, response):
       ...
       response.set_cookie(settings.CSRF_COOKIE_NAME,
            request.META["CSRF_COOKIE"], max_age = 60 * 60 * 24 * 7 * 52,
            domain=settings.CSRF_COOKIE_DOMAIN,
            secure=settings.SESSION_COOKIE_SECURE or None)

Direct HTTP requests to HTTPS in the webserver

Next you want a rewrite rule that redirects http requests to https, e.g., in nginx

server {
   listen 80;
   rewrite ^(.*) https://$host$1 permanent;
}

Django's reverse function and url template tags only return relative links; so if you are on an https page your links will keep you on the https site.

Set OS environmental variable HTTPS to on

Finally, (and my original response excluded this), you need to enable the OS environmental variable HTTPS to 'on' so django will prepend https to fully generated links (e.g., like with HttpRedirectRequests). If you are using mod_wsgi, you can add the line:

os.environ['HTTPS'] = "on"

to your wsgi script. If you are using uwsgi, you can add an environmental variable by the command line switch --env HTTPS=on or by adding the line env = HTTPS=on to your uwsgi .ini file. As a last resort if nothing else works, you could edit your settings file to have the lines import os and os.environ['HTTPS'] = "on", which also should work.

If you are using wsgi, you may want to additionally set the environmental variable wsgi.url_scheme to 'https' by adding this to your settings.py :

os.environ['wsgi.url_scheme'] = 'https'

The wsgi advice courtesy of Vijayendra Bapte's comment.

You can see the need for this environmental variable by reading django/http/__init__.py:

def build_absolute_uri(self, location=None):
    """
    Builds an absolute URI from the location and the variables available in
    this request. If no location is specified, the absolute URI is built on
    ``request.get_full_path()``.
    """
    if not location:
        location = self.get_full_path()
    if not absolute_http_url_re.match(location):
        current_uri = '%s://%s%s' % (self.is_secure() and 'https' or 'http',
                                     self.get_host(), self.path)
        location = urljoin(current_uri, location)
    return iri_to_uri(location)

def is_secure(self):
    return os.environ.get("HTTPS") == "on"

Additional Web Server Things:

Take that guy's advice and turn on HSTS headers in your web server by adding a line to nginx:

add_header Strict-Transport-Security max-age=31536000;

This tells your web browser that your website for the next 10 years will be using HTTPS only. If there's any Man-in-the-middle attack on any future visit from the same browser (e.g., you log on to a malicious router in a coffee-shop that redirects you to an HTTP version of the page), your browser will remember it is supposed to be HTTPS only and prevent you from inadvertently giving up your information. But be careful about this, you can't change your mind and later decide part of your domain will be served over HTTP (until the 10 years have passed from when you removed this line). So plan ahead; e.g., if you believe your application may soon grow in popularity and you'll need to be on a big CDN that doesn't handle HTTPS well at a price you can afford, you may have an issue.

Also make sure you disable weak protocols. Submit your domain to an SSL Test to check for potential problems (too short key, not using TLSv1.2, using broken protocols, etc.). E.g., in nginx I use:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS";
19
  • Theoretically though, if you hit an http page, an attacker could change the redirect to an attacker controlled page, right? They can't steal the cookie, but they could still do damage.
    – mikeazo
    Commented Nov 16, 2011 at 23:16
  • mikeazo: I agree if a user goes to example.com that's supposed to redirect to example.com an attacker can spoof my site to one that redirects to another https server which they potentially even have legitimately signed certificates for. However, this problem can't be solved; the user tried going to an unsecured site. (Even if I just disable my http version; if users still occasionally go to the http version an MITM attacker could always redirect those people.) Hopefully users would notice the different domain name or just learn to use the https version only.
    – dr jimbob
    Commented Nov 17, 2011 at 15:12
  • 3
    @drjimbob, part of the problem is that DJango is creating internal links - when I do an HttpResponseRedirect to a reversed view name, the end result has a http-prefix. Now, I have just coded a decorator that replaces http with https, so Django will redirect properly. Question, though - I didn't find CSRF_COOKIE_SECURE in the Django 1.3 documentation, where does it come from? Thanks.
    – John C
    Commented Nov 17, 2011 at 19:13
  • 1
    @Kave - Thanks. You need to set an environmental variable with a key of HTTPS to on. So if you call uwsgi from the command line via first example on django uwsgi page, you'd need to add the flag --env HTTPS=on. Or if you had an uwsgi.ini file you'd add the line: env = HTTPS=on. (The configuration is exactly identical to what you do for setting the environmental variable DJANGO_SETTINGS_MODULE. Just replace DJANGO_SETTINGS_MODULE with HTTPS and its value with on).
    – dr jimbob
    Commented Apr 3, 2013 at 15:30
  • 1
    It might be worthwhile to mention including the HSTS header (as suggested by @that-guy-from-over-there). More on that here: mikkel.hoegh.org/blog/2010/09/09/… Commented Jan 16, 2014 at 21:38
3

Redirecting from any http:// to the corresponding https:// page is the wrong approach. Configure nginx to redirect port 80 to https://yourdomain.ext/

server {
       listen 80;
       rewrite ^/? https://$host/ permanent;
 }

or similar (check the next nginx manual near you) and do not run your application at all on port 80 (http). So, other requests on port 80 resolve to a 404 or similar (customize it, saying that your app is now secure and runs only on https with a link pointing to https://yourdomain.ext/). Then run your app only on listen port 443 (https). Using relative paths in you code is now secure, since they all resolve to the full https:// path and you avoid the http to https bouncing!

1
  • 1
    Unfortunately, that's exactly how I do it - I have nginx configured to listen on 80, rewrite to https, and another server block that listens on 443 and passes thru to Django (well, uWsgi). Yet it's Django generating the http links (and I have not yet found how to change that). I don't see how they resolve to https, without going through the client browser first :(
    – John C
    Commented Nov 17, 2011 at 13:10
3

A common setup will have you forwarding https traffic from your webserver (i.e. Nginx) to a local http server running the Django app.

In this case it will be easier to use the SECURE_PROXY_SSL_HEADER setting (available since Django 1.4.)

https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SECURE_PROXY_SSL_HEADER

3

you should additionally send a HSTS-Header from nginx, indicating to the clients (browsers) they shall use HTTPS only

add_header Strict-Transport-Security max-age=31536000;
1
  • This HSTS-Header can can also be set in Django, though note the Django docs advise "it’s a good idea to first use a small value for testing, for example, [...] 3600 for one hour" incase this setting breaks anything. I assume the same would apply for the above.
    – Jonny
    Commented Nov 17, 2016 at 9:09
2

I think what you're looking for is a Django middleware that will rewrite http to https. Something similar to what is addressed in this question on SO, where one answer points to this middleware. You'll probably have to write your own middleware, but it should be straightforward. (A well-focused question on SO will get you pointed in the right direction if you need help getting started.)

2

In most cases you can set Apache or something to redirect to https, as described in the accepted answer. And if you can, that would be better, for performance and for files served outside Django.

But if you cannot, or want to do debugging, then I would like to point out that Django recently (1.8) introduced a SecurityMiddleware which has https-redirects as one of it's several functions.

More info is available in the documentation. Basically, add django.middleware.security.SecurityMiddleware and set SECURE_SSL_REDIRECT = True.

(The header mentioned by the accepted answer can also be set by this middleware.)

1

You need to configure django to generate either

  1. https://domain/path links with the https: scheme,
  2. //domain/path links with no scheme (the browser will interpret these as having the same scheme as the page it's currently opened to), or
  3. /path links with no scheme or domain (the browser will interpret these as having the same scheme and domain as the page it's currently pointed to).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .