Django example: Setting up HTTP security

My project this weekend was to optimize my site for security, and performance to prepare for an upcoming launch. Although I hadn’t been ignoring these things completely, I figured it was a good chance to step back and check where I was at and tweak where necessary. In this post I’m going to go over the steps I took to implement security measures in my Django site.

My focus on security for this weekend was specifically on HTTP security. I’ve never really implemented HTTP security before, so for me this was totally a learning experience. I started off by adding an SSL cert to my Heroku server. They make it terribly easy to do since a cert comes free with any paid server. So after upgrading my Heroku server to the paid Hobby dyno I was feeling pretty good about myself. Security issues solved, right?

hacker on computer
This stock photo hacker ain’t gettin’ into my server.

Okay, maybe not. A quick scan using Observatory by Mozilla made all my dreams of becoming a security super hero come crashing to a halt. My site had a big, fat F. (That’s for Fail, not Fantastic.) So what did I need to work on?

Failing grade from Observatory

Basically? Everything. Applying that cert may have been easy, but it didn’t do much for my grand score on Observatory. So here’s a break down of how I eventually got a passing grade.

The low-hanging fruit: XXS and Content-Type options

Let’s start with the easy ones. The X-XSS-Protection header helps some browsers to detect and defend against cross-site scripting attacks. According to Mozilla, the header is slightly outdated in comparison to implementing a comprehensive CSP header (more on that later), but it’s still a good idea to implement. In Django it’s dead simple to implement by just setting the SECURE_BROWSER_XSS_FILTER setting to True.

The X-Content-Type-Options header, when set to nosniff, requires that the server indicate the correct MIME type on stylesheets and scripts before it will load them, which protects the site from cross-site scripting. Using Django this requirement can be satisfied by setting the SECURE_CONTENT_TYPE_NOSNIFF setting to True.

These two settings alone raised my grade from an F to a D. I was on a roll.

Next step: turn on HTTPS

Next let’s look at HTTP redirection. Just having an SSL certificate alone isn’t enough to tell browsers to access your site via HTTPS. Luckily, there are again a few handy Django settings to make HTTPS somewhat easy to implement.

By setting SECURE_SSL_REDIRECT, SESSION_COOKIE_SECURE, and CSRF_COOKIE_SECURE to True I now had my site redirecting to HTTPS and my cookies were secure. Additionally, as small change to my DNS setup to make sure that I was directing it to the secure protocol initially instead of directing first to HTTP.

With these changes my grade is now up to a C! No sweat!

The scary step: HTTP Strict Transport Security

The truth is, I was totally nervous about setting my site’s HSTS policy. When this policy is set, browsers will refuse to connect to your site for the given time period if you’re not properly serving HTTPS resources, or if your certificate expires. Users won’t even have a way of getting around the warning like they normally do. Django has warnings all over their documentation about HSTS settings, which was adding to my wariness. In the end, I decided to turn on the settings, but leave it at a fairly low level until I’m more comfortable with it. I set SECURE_HSTS_SECONDS to 86400 (one day), and I turned on SECURE_HSTS_INCLUDE_SUBDOMAINS and SECURE_HSTS_PRELOAD.

With these settings turned on my score rose to a C+. I was still getting dinged on the HSTS setting for having a setting of less than 6 months, but for now I’ll take it.

By the time I finished these steps, here’s what my settings look like:

SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 86400 # 1 day
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

The failed attempt: Implementing Subresource Integrity

My next step was to attempt to implement Subresource Integrity. Subresource Integrity is designed to protect your server in case a CDN you use for loading external resources were to be hacked. The idea is that if you put a certain key on the resource then any changes to the underlying file will trigger the browser to refuse to load that resource. This works well for static, versioned documents like jQuery, but doesn’t work well for documents that are dynamic and changed regularly such as fonts and Google Analytics scripts.

Still, I made an effort to implement this requirement by applying hashes provided by SRI Hash Generator to a couple of external scripts that I was loading. In the end, though, those 5 Observatory points are just going to have to elude me for the time being.

The hard part: Content Security Policy

Despite my failure to get those 5 points, I was still feeling optimistic. I had only one step left: implementing a Content Security Policy.

I stumbled a bit from the beginning when I learned that Django does not have a built-in method for creating a csp header. A quick Google search, however, lead me to find Mozilla’s django-csp module which I installed. Naïvely, I thought “how hard can this be? Just turn on a couple of settings and we’re done, right?”. So wrong. I turned on the settings, restarted my app and refreshed my site only to see… basically nothing. Everything was broken. From here it was just a matter of drudging through every error and applying the correct fix.

As suggested by Mozilla, I began by setting everything to the strictest setting possible: only allowing resources to be loaded from the current origin:

CSP_DEFAULT_SRC = ("'none'", )
CSP_STYLE_SRC = ("'self'", )
CSP_SCRIPT_SRC = ("'self'", )
CSP_IMG_SRC = ("'self'", )
CSP_FONT_SRC = ("'self'", )

Inline scripts and styles

Let’s work now on the part, which is to deal with inline scripts and styles. One aspect of CSP is that all inline scripts and styles are disallowed by default. This means ever little one-off <script> you've added to get something to work quickly, every onfocus="dosomething()" you've added to an input field, every time you used style="padding-top:2px" to make something align just right is no longer allowed. Every script and style must be loaded from a resource or explicitly allowed in your policy. Sure, you could add 'unsafe-inline' to your script and style CSP headers - but doing so basically negates the whole policy. I was determined that I would get all 25 points for CSP on Observatory's score.

Creating an inline script and style policy is a great time to review your site to make sure you’re following best practices and clean code. I started by fixing my over-use of style="display-inline: block", which was easily fixed with Bootstrap's built-in display property utility classes, and I worked on removing uses of inline JavaScript events by instead setting these properties within my javascript files themselves. But some things are just out of my control, like the inline script that Google Analytics insists on pumping into the <head> of your page and the inline style="position: absolute" that the Gulp SVG sprite generator puts on the top of every sprite file. It turns out though that there are ways of allowing these few one-off inline scripts and styles.

The first method is to add a hash code for the element to your CSP policy. This was the easiest way for me to allow that inline style generated in the sprite file. Chrome makes it easy by supplying the requisite hash value straight in the console error message:

Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self' fonts.googleapis.com". Either the 'unsafe-inline' keyword, a hash ('sha256-/3kWSXHts8LrwfemLzY9W0tOv5I4eLIhrf0pT8cU0WI='), or a nonce ('nonce-...') is required to enable inline execution.

So all I needed to do for that pesky inline style was add that value to my CSP_STYLE_SRC. No sweat!

It gets a little more complicated when I attempted to fix the Google Analytics script. For this script I’m supplying the Analytics key dynamically based on some settings in my site, so in case my key needs to change or I have a different key for, say, a subdomain, I can easily change the key without need to make a code change. The problem here is that the hash value only works if the script stays exactly the same, meaning I lose the capability of having a dynamic key.

This is where the nonce comes in. According to this article by Troy Hunt, using a nonce value is a way to protect your site from attacks by generating a random, single-use code in your csp policy that you can use to identify trusted scripts in your site. It’s a good way to allow those inline scripts that you just have to make dynamic.

Although it isn’t documented, django-csp has support for nonce codes, which I was able to find by digging through their source. To use it I added CSP_INCLUDE_NONCE_IN = ('script-src',) to my settings file, which adds the single-use code to the script section of the CSP header. Next, on the <script&gt tag of the inline Google Analytics code I added the attribute nonce="{{ request.csp_nonce }}". With that, a generated nonce value was added to the CSP header and the same value was added as an attribute on the script.

Finally, certain inline images are also disallowed, such as those using data URIs. In order to allow these images to be loaded I added “data:” to my CSP_IMG_SRC list.

At this point my CSP settings looked like this:

CSP_DEFAULT_SRC = ("'none'", )
CSP_STYLE_SRC = ("'self'", "'sha256-/3kWSXHts8LrwfemLzY9W0tOv5I4eLIhrf0pT8cU0WI='")
CSP_SCRIPT_SRC = ("'self'", )
CSP_IMG_SRC = ("'self'", "data:")
CSP_FONT_SRC = ("'self'", )
CSP_INCLUDE_NONCE_IN = ("script-src", )

And my Google analytics script had a slight change to include the nonce:

<script nonce="{{ request.csp_nonce }}">
<!-- Google Analytics stuff here -->
</script>

External resources

The final step is to allow certain external resources to be loaded on the page. Technically the best practice here would be to add a nonce value to the scripts such as:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js" nonce="dbOYAczO25cy0LFv"></script>

Doing so would ensure that just that resource allowed from that domain. However, as noted in Troy Hunt’s article, using a nonce on inline scripts does not work in Edge. The next best option is to add a whitelist of allowed domains where external resources can be loaded from.

Between jQuery, Google Analytics, and Google Fonts I’m loading quite a few resources through Google’s various CDN resources, which basically meant a lot of Google URLs needed to be allowed in my CSP policy.

These changes, along with some final recommendations from Observatory and Google’s CSP Evaluator, left my final CSP setup looking like this:

CSP_DEFAULT_SRC = ("'none'", )
CSP_STYLE_SRC = ("'self'", "fonts.googleapis.com", "'sha256-/3kWSXHts8LrwfemLzY9W0tOv5I4eLIhrf0pT8cU0WI='")
CSP_SCRIPT_SRC = ("'self'", "ajax.googleapis.com", "www.googletagmanager.com", "www.google-analytics.com")
CSP_IMG_SRC = ("'self'", "data:", "www.googletagmanager.com", "www.google-analytics.com")
CSP_FONT_SRC = ("'self'", "fonts.gstatic.com")
CSP_CONNECT_SRC = ("'self'", )
CSP_OBJECT_SRC = ("'none'", )
CSP_BASE_URI = ("'none'", )
CSP_FRAME_ANCESTORS = ("'none'", )
CSP_FORM_ACTION = ("'self'", )
CSP_INCLUDE_NONCE_IN = ('script-src',)

And with that, my final Observatory grade was an A-!

A passing grade from Observatory

Props to Mozilla for gamifying HTTP security — I’m not sure I would’ve stuck to this project if I wasn’t so obsessed with watching my score rise. Although I know there’s a lot more to do to make sure my site is truly secure, this was definitely a step in the right direction, and it took less than a day!

Have you implemented an HTTP policy for your site? Let me know in the comments!


Originally published at Andrea Robertson.