How to Score A+ for Security Headers on Your Django Website
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:
While (at time of writing) Google scores a C:
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…
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.
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.
- You need
MIDDLEWAREsetting, as high as possible. This is already done for you in the default
SECURE_BROWSER_XSS_FILTER = Truein your settings file.
See more in the Django documentation, for example caveats with media files.
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.
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:
includeSubDomainsincludes all subdomains of your domain.
preloadtells 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 URL’s 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 TLD’s, 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
SecurityMiddlewareinstalled as above.
SECURE_HSTS_SECONDSto the number of seconds you want to specify in the header.
- Optionally, set
Trueto 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
For some more information see Django’s HTTP Strict Transport Security documentation.
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!
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
This header is also built in to Django, and you’ll be seeing the
security.W006 warning if you haven’t enabled it.
SecurityMiddlewareinstalled as above.
SECURE_CONTENT_TYPE_NOSNIFF = Truein 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.
…okay, let’s finish this!
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,
To enable it and block your site from ever being in an
- You need
MIDDLEWAREsetting, as high as possible. This is already done for you in the default
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.
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. URL’s often leak information, for example
/firstname.lastname@example.org. Also if a user visits an HTTP site from your HTTPS-only one, AITM’s could read those private URL’s.
Referrer-Policy header allows us to tell the browser when to send
(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.)
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.
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:
- By default, only load resources from the current domain
- Apart from images, which can load from any domain
- Media can only load from
media2.com(and not the current domain)
- Scripts can only load from
You send the policy in the
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.
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.
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
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.
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
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).
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 your Django project’s long test runs bore you, I wrote a book that can help.
One summary email a week, no spam, I pinky promise.
- Scoring A+ for Security Headers on My Cloudfront-Hosted Static Website
- Feature-Policy updates - now required for an A+ on SecurityHeaders.com