Trying to make a Django-based site use HTTPS-only, not sure if it's secure?

  • 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.

    How do you have the form's action attribute setup? If you force it to the full https url does that fix it?

    @mikeazo, the HTML syntax I use is `

    {% csrf_token %} `. The actual redirection is done in the Python view code, and the URL is determined by Django.

    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

    @JohnC fixed (njinx) typo for nginx.

  • dr jimbob

    dr jimbob Correct answer

    10 years ago

    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";
    

    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: 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.

    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.

    @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.

    Sigh... I just found CSRF_COOKIE_SECURE. For some reason, the standard Django Docs 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. :)

    @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, 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.

    @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.

    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, and I'll checkmark it.

    @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.

    This is awesome, thanks! One important note in your settings.py **DEBUG** must be **False**

    @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.

    @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

    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.

    @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`).

    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/

    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

    @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.

    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?

License under CC-BY-SA with attribution


Content dated before 7/24/2021 11:53 AM