68

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.

John C
  • 1,207
  • 2
  • 11
  • 15
  • How do you have the form's action attribute setup? If you force it to the full https url does that fix it? – mikeazo Nov 16 '11 at 21:03
  • @mikeazo, the HTML syntax I use is ` – John C Nov 16 '11 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 – Sachin Kumar Nov 17 '11 at 06:26
  • @JohnC fixed (njinx) typo for nginx. – dr jimbob Nov 17 '11 at 14:55

7 Answers7

71

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";
dr jimbob
  • 38,768
  • 8
  • 92
  • 161
  • 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 Nov 16 '11 at 23:16
  • mikeazo: I agree if a user goes to http://example.com that's supposed to redirect to https://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 Nov 17 '11 at 15:12
  • right, but in this case, the user is not intentionally going to an unsecured webpage. The user goes to a secure webpage, clicks submit, is automatically sent to an unsecure webpage, and is automatically redirected to a secure page. – mikeazo Nov 17 '11 at 16:21
  • 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 Nov 17 '11 at 19:13
  • Sigh... I just found [CSRF_COOKIE_SECURE](https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#csrf-cookie-secure). For some reason, the standard [Django Docs](https://docs.djangoproject.com/en/1.3/) didn't want to find it. Apparently it's in the Development docs, but not Version 1.3 docs. Now I have to decide if its worth downloading the later version. :) – John C Nov 17 '11 at 19:25
  • @JohnC I am just curious. Did this solution help you solve your problem? Specifically with the internal link is it still http or https now? Thanks. – CppLearner May 18 '12 at 17:21
  • @CppLearner, I suspect my question wasn't very clear. Django appeared to generate some links using an `http` prefix, which the nginx server re-directed to `https` - and all I was wondering, was if there were any security problems with this process. I have since written some middleware that prepends all Django internally-generated pages with an `https`, so the question isn't really relevant (at least to me). And jimbob, I wasn't actually *trying* to use `http` in part of the site, it simply seemed that Django had no other option, at the time. – John C May 18 '12 at 17:53
  • @JohnC Thank you for the quic response, as well as to dr jimbob. I am going to try this later this week. JohnC, your question is definitely something I have been wondering as well. Thanks. – CppLearner May 18 '12 at 19:12
  • dr jimbob, edit-4 is an *excellent* solution to the problem, much better than writing a middleware component. Would you mind adding it as an answer to [my question here](http://stackoverflow.com/q/8015685/78409), and I'll checkmark it. – John C May 19 '12 at 20:59
  • @JohnC - Sorry for the incorrect original responses. I wasn't I was using any `HttpRedirectResponse` in my original apps and never had the issue; but once you brought it up; did a quick search of the source code and found the fix. I've reformatted the answer here and also put on SO. – dr jimbob May 19 '12 at 23:29
  • This is awesome, thanks! One important note in your settings.py **DEBUG** must be **False** – Gourneau Dec 22 '12 at 07:59
  • @Gourneau - Why do think `DEBUG` must be `False`? Obviously it should be in a secure application (giving attackers debugging info helps them immensely) but every part of the answer seems to work regardless of the debug mode setting. – dr jimbob Dec 22 '12 at 21:16
  • @drjimbob You are right. I mistook this for another problem. When using heroku request.is_sercure() always returns False due to their reverse http proxies. This was causing the APPEND_SLASH to redirect to http instead of https. The solution is to add `SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https',)` to settings.py – Gourneau Dec 22 '12 at 22:40
  • great solution. I use nginx + uwsgi instead of mod_wsgi. How do I set `os.environ['HTTPS'] = "on"` in uwsgi please? that part wasn't very clear to me. – Houman Apr 03 '13 at 07:56
  • 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](https://docs.djangoproject.com/en/1.4/howto/deployment/wsgi/uwsgi/), 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 Apr 03 '13 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: http://mikkel.hoegh.org/blog/2010/09/09/protecting-your-users-phishing-apache-rules-hsts/ – Dolan Antenucci Jan 16 '14 at 21:38
  • If you are using wsgi to host your django app then you need to set one environment variable: environ['wsgi.url_scheme'] = 'https' for https://github.com/django/django/blob/master/django/core/handlers/wsgi.py#L115 – Vijayendra Bapte Feb 05 '14 at 01:09
  • @VijayendraBapte - Will add that. Note they still have the setting `environ['HTTPS'] = 'on'` in other places (for example see: https://github.com/django/django/blob/master/django/http/request.py#L137-L138 ). Granted, I'm not sure the logic if your application makes `WSGIRequest`s or `HttpRequest`s when and if the `_get_scheme` in `HttpRequest` is overridden. Probably safest to use both. – dr jimbob Feb 05 '14 at 04:39
  • The recommendation is to add `environ['HTTPS'] = 'on'` to wsgi.py and `os.environ['wsgi.url_scheme'] = 'https'` to settings.py. Can both of these safely go in wsgi.py? – epalm Dec 19 '14 at 16:40
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

Sebastian
  • 131
  • 2
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;
  • This HSTS-Header can can also be set in Django, though note the [Django docs advise](https://docs.djangoproject.com/en/1.10/ref/middleware/#http-strict-transport-security) "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 Nov 17 '16 at 09:09
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!

esskar
  • 629
  • 1
  • 5
  • 12
  • 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 Nov 17 '11 at 13:10
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.)

bstpierre
  • 4,868
  • 1
  • 21
  • 34
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.)

Mark
  • 333
  • 2
  • 8
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).
yfeldblum
  • 2,807
  • 20
  • 13