How to Score A+ for Security Headers on Your Django Website

Make your website as secure as this castle!

This is a blog post version of the talk I gave at DjangoCon Europe 2019 on the 10th April.

The web is an evolving platform with a lot of backwards compatibility concerns. New web security practices often come from a realization that an old feature has some flaw. Rather than break old websites by changing such features, there are a bunch of more secure behaviours to opt in to. You can do this by setting HTTP headers.

Securityheaders.com is a tool run by security consultant Scott Helme to create a report on these security headers. It gives any URL a score from F to A+, which is a nice simple way of measuring your security posture. Though, like any automated report, it needs need some human interpretation to factor in context.

(I’d also recommend the Mozilla Observatory security scanner, but I’m not using it here because it does way more than security headers.)

As an example, Yahoo scores an A+ on Securityheaders.com:

yahoo.com being checked on Securityheaders.com

While (at time of writing) Google scores a C:

google.com being checked on Securityheaders.com

This is a guide on how to configure a typical Django web application to score that magic A+. You can beat Google, protect your users, and impress your boss, clients, or parents!

We’ll look at the 6 headers you need to set to score an A+ (at time of writing). Plus, we’ll cover a bonus experimental 7th header…

1. X-XSS-Protection

Update (2019-08-02): While the header is still recommended, Securityheaders.com removed its requirement for an A+ grade, so this is a bonus now :)

Cross-Site Scripting, or XSS, is a technique an attackers can use to inject their own code into your website. This might do something naughty, like add false content or spy your users to steal their passwords.

Since XSS is such a common flaw in websites, browsers have added features to detect and prevent it in some cases, bundled in their “XSS Auditors”. These are on by default, so bits of script that look like an XSS attack get blocked, but the page continues to work.

Setting the X-XSS-Protection header to use “block mode” provide extra security. This tells the browser to completely block pages with detected XSS attacks, in case they contain other bad things. For an example, see Scott Helme’s demo where HTML sent in a GET param and appears on the page which then gets blocked.

This provides a little extra protection, and is a good idea to add. Even if your website is secure to XSS, if it triggers a browser’s XSS auditor it should be refactored to avoid doing so.

Django has this header built-in, and it’s easy to activate. You’ll already be seeing the security.W007 warning when you run manage.py check --deploy if you haven’t set it up.

To enable:

  1. You need django.middleware.security.SecurityMiddleware in your MIDDLEWARE setting, as high as possible. This is already done for you in the default startproject template.
  2. Add SECURE_BROWSER_XSS_FILTER = True in your settings file.

See more in the Django documentation, for example caveats with media files.

2. Strict-Transport-Security

HTTP Strict Transport Security, or HSTS, is a way of telling the browser to load your site over HTTPS only. Once a browser has seen the header on a website, it will only make HTTPS requests to that website. The header includes a max age in seconds, which limits how long the browser will remember to do this. This prevents an Attacker-In-The-Middle, or AITM, from intercepting HTTP requests to serve evil content on your domain.

Setting the Strict-Transport-Security header with a max-age value opts in to this behaviour. On top of this you can set a couple of flags:

  1. includeSubDomains includes all subdomains of your domain.

  2. preload tells browsers to store your domain in a database of known strict-only domains. It can only be set on top level domains like example.com. Browsers opening URLs on HSTS-preloaded domains will never make AITM-able HTTP requests.

    Once this flag is set, you submit your domain to all browsers through Google’s preload service. After acceptance, the next versions of each browser will include your domain.

These days some TLDs, such as .app, are themselves preloaded, meaning they only support HTTPS sites. Super secure.

Again, this header is built-in to Django. You’ll see the security.W004 warning if you haven’t set the max age setting, security.W005 for the includeSubDomains flag, or security.W021 for the preload flag.

To enable:

  1. Have SecurityMiddleware installed as above.
  2. Set SECURE_HSTS_SECONDS to the number of seconds you want to specify in the header.
  3. Optionally, set SECURE_HSTS_INCLUDE_SUBDOMAINS and SECURE_HSTS_PRELOAD to True to activate their respective flags.

This isn’t something you can simply turn on, especially if other subdomains are in use! The only time it’s straightforward is if you have a completely new domain. Django’s warning security.W004 even says:

… enabling HSTS carelessly can cause serious, irreversible problems

If you prematurely activate it, you will block users making legitimate HTTP requests. The browser will lock them out until you remove the header and the max age seconds have passed.

Because of this, you should slowly ramp up SECURE_HSTS_SECONDS, checking nothing breaks each time. Do this across multiple deployments on multiple days, waiting for user feedback to get to you. Start at something small, like 30 seconds, and work up to 31536000 seconds (1 year).

Adding the flags depends on your situation. If it never makes sense to enable one, disable the respective warning with SILENCED_SYSTEM_CHECKS.

For some more information see Django’s HTTP Strict Transport Security documentation.

3. X-Content-Type-Options

Browsers try to guess the content type of responses if the server seems to send the wrong one, a feature called MIME Sniffing. This backfires wrong when the browser guesses the wrong content type. For example, if a user-uploaded image on your site is interpreted as HTML, it allows XSS.

Originally, each browser had different rules, but they’re aligning under a WHATWG specification. It’s many rules though, so it’s hard to predict and test!

Setting the X-Content-Type-Options header to nosniff opts out of MIME sniffing (in most circumstances). Since a well-built website won’t need this behaviour, you should always use nosniff.

This header is also built in to Django, and you’ll be seeing the security.W006 warning if you haven’t enabled it.

To enable:

  1. Have SecurityMiddleware installed as above.
  2. Set SECURE_CONTENT_TYPE_NOSNIFF = True in your settings.

See more in the X-Content-Type-Options Django documentation.


We're half way! Let's take a break to admire these flowers.

Some lovely flowers

…okay, let’s finish this!


4. X-Frame-Options

Clickjacking is a technique where an attacker tricks your user into clicking something on your site. This is typically done by embedding your site in an <iframe> on the attacker’s site.

For an example, see Troy Hunt’s blog post. It describes a banking application placed transparently in a frame in front of a “Win an iPad” button. When the user tries to claim the prize, they are actually clicking the “transfer money” button on the bank website.

There are various techniques to prevent clickjacking, but the best is to add the X-Frame-Options header. This allows you to disable your site from ever being in an <iframe>, or only on an allow list of trusted domains.

This header is also built-in to Django. You’ll see the security.W002 warning if you don’t have the middleware installed, or security.W019 if you don’t have it set to its most secure option, DENY.

To enable it and block your site from ever being in an <iframe>:

  1. You need 'django.middleware.clickjacking.XFrameOptionsMiddleware' in your MIDDLEWARE setting, as high as possible. This is already done for you in the default startproject template.
  2. Set X_FRAME_OPTIONS = 'DENY' in your settings.

There are extra features to enable certain domains to <iframe> your site, or to disable the protection on certain views. See them in the Django Clickjacking Protection documentation.

5. Referrer-Policy

The Referer header communicates to a website the URL the user came from. This is great for analytics, as you can discover where your site visitors are coming from by logging these values.

It is also terrible for privacy, as websites you link to receive which URL users came from. URLs often leak information, for example adamj.eu/illuminati-funding or /staff-admin/users/?q=user@example.com. Also if a user visits an HTTP site from your HTTPS-only one, AITMs could read those private URLs.

Setting the Referrer-Policy header allows us to tell the browser when to send Referer.

(Caution: the word “referrer” is has two “r’s” in the middle. The original HTTP specification had a typo with one “r” and no one noticed, so the header is spelled Referer. To not repeat this misspelling, the authors of the Referrer-Policy were careful to include two “r’s”. This is more confusing. “Referer” with one “r” is already in wiktionary, I think we should have stuck with it. And the bike shed should be yellow.)

Update (2020-02-04): James Bennett added this header to Django 3.0, controlled by the SECURE_REFERRER_POLICY setting. You no longer need the external package referenced below.

This header is not built in to Django, but you can add it with the django-referrer-policy package, by fellow core developer James Bennett. Follow the instructions in the package documentation to add its middleware and setting.

Be warned, the “most secure” value no-referrer breaks Django’s CSRF (see “Removing the Referer header” in the CSRF docs). You probably don’t want to do this.

I’d recommend using same-origin, if you can, which sends Referer only for requests to the same domain. This allows CSRF and internal analytics to work without leaking Referer values to other domains.

6. Content-Security-Policy

This is a big one. Content Security Policy, or CSP, is a policy that blocks some content. This helps stop XSS, clickjacking, and other kinds of injection attacks.

By default browsers allow websites to load content from anywhere. This means if an attacker successfully performs an XSS attack on your website, they can embed code from anywhere on the internet. A CSP is a set of directives that define allow lists of domains that ther browser may load content from. This severely restricts such attacks.

For example, take this policy from the MDN documentation:

default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com

There are four directives here, separated by semicolons. They tell the browser:

You send the policy in the Content-Security-Policy header.

This header is not built-in to Django, but it can be set with the django-csp package from Mozilla. Follow the installation and configuration guides there. It includes a middleware plus many settings to build up the policy directives.

As for creating your policy, well, I did say this is a big one. There are a lot of different directives and options in the CSP specification, and django-csp uses 24 different settings.

I’d recommend reading the MDN docs and getting a grasp on the options here, but here are some guidelines.

Update (2020-07-27): For a thorough guide to setting up django-csp, see this blog post by Steven Pate.

A Greenfield Site

If you’re on a brand new greenfield site, with no external resources, it’s easy. You should start with a restrictive CSP and open it up as you develop the site.

External resources of all kinds won’t load, and when this happens you’ll see errors in the browser’s devtools console. Then you can adjust your design or CSP settings appropriately.

You could start with django-csp’s default settings, which has just CSP_DEFAULT_SRC = ["'self'"]. This blocks most external resources.

Or you could start with a more restrictive CSP, for example that recommended by Google’s Strict CSP Page. This will protect your site more.

An Existing Site

CSP is harder to add to an existing site, and the bigger the site, the bigger the task. You should do this iteratively.

The first step is to make an initial CSP. You could use one of the restrictive recommendations above, or make a more educated guess with a tool like CSP Toolkit.

Then, deploy this in report only mode, which you can do by setting django-csp’s CSP_REPORT_ONLY and CSP_REPORT_URI settings. Your users’ browsers will inspect but not enforce the policy, and then report violations back to you. This is nice, since you won’t need to check the whole site yourself!

Activate read only mode on django-csp by setting CSP_REPORT_ONLY = True and CSP_REPORT_URI to a webhook URL. This URL will receive the reports back in JSON format. You can try receive the reports yourself, but I wouldn’t recommend it.

Instead use a service that can handle ingesting all the reports and present them to you in a nice dashboard. The two I know of are:

  • the aptly named report-uri.com, by the creator of Securityheaders.com
  • Sentry, a tool known for debugging server-side errors

Once you have this reporting in place, you can iterate the CSP until you see no more violations. Then tell browsers to enforce it by disabling report-only mode. Reporting is still useful after this as your site changes, or is subject to XSS attacks.

7. Bonus Feature-Policy

Rabbit

Update (2019-08-02): While the header is still experimental, Securityheaders.com updated to require it for an A+ grade, so this is not really a bonus now :)

Doing the above six headers will get you an A+ rating on Securityheaders.com. This is the bonus final header that can help you secure your site further, but it’s currently experimental.

Feature Policy is another policy, like CSP, that controls browser features, such as video autoplay or webcam access. It allows you to disable them for your site or those in your <iframe>s. It’s sent in the Feature-Policy header. Scott Helme’s blog post is a good introduction.

At time of writing, it’s not well supported. Chrome is the main browser implementing it but it requires the user to enable the “experimental web features” flag. It’s also fast moving. That said, it’s good to know about it and be ahead of the curve!

To enable it in Django, use the django-feature-policy package (by yours truly). This requires another middleware and the setting FEATURE_POLICY. See the package documentation, plus the MDN documentation for what each of the features does.

I’d recommend totally disabling some annoying features like autoplay, camera, fullscreen, geolocation, and microphone (assuming your site doesn’t use them). This means that scripts you embed, for example from advertising partners, can’t annoy your users with these features.

While it won’t protect many users, you can develop the site with the “experimental web features” flag on to ensure you’re following these best practices. The devtools console shows log messages when the browser blocks a feature.

Since the header is experimental, if you do add it, keep django-feature-policy up to date. I’m updating it regularly to follow changes to the upstream specification. I recently released 2.0.0 because a bunch of the features changed (see the changelog).

We’re Done!

I hope that has helped you level up your web security knowledge and that you can find time to move your website to an A+ score! (Or at least to decide where to stop in a more informed manner 😉.)

If you used this post to improve your site, I’d love to hear your story and add your site to the follow-up Hall of Fame post. Tell me via Twitter or email - contact details are on the front page.

—Adam

Peacock feathers

Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: