Security Tip: Security Headers are Layers of Defence

[Tip#46] Security headers add important layers of defence to your apps, preventing data leaks, XSS and CSRF attacks, clickjacking, and more... Why are you leaving your apps unprotected?

Security Tip: Security Headers are Layers of Defence

Greetings friends! We’re over halfway, down to #4 in the Top 10 security issues I’ve found during audits. This week is Missing Security Headers! This a big topic to fit into a tip, so my goal today is to get you started and point you in the right direction.

Here’s our current Top 10:
#10 → Insufficient Input Validation
#9 → Missing Subresource Integrity (SRI)
#8 → Insufficient Rate Limiting
#7 → Cross-Site Scripting (XSS)
#6 → Outdated & Vulnerable Dependencies
#5 → Insecure Function Use
#4 → Missing Security Headers

🛡️ When was your last annual security audit?
🕵️ Book a security audit now to keep up-to-date!

Looking to learn more?
OWASP Security Tip: A03:2021 – Injection
▶️ OWASP In Depth: A01:2021 – Broken Access Control


Security Headers are Layers of Defence

There is a common concept in the security industry known as Defence in Depth or Security in Depth, which can be summed up simply as having multiple defences layered on top of each other, so if one fails, the next one keeps you protected. This is the key idea behind security headers within browsers. With one notable exception1, they protect your app if your other defences fail, sometimes due to factors outside your control.

The best place to get started with security headers is with the excellent Security Headers scanner project by Scott Helme. The way it works is really simple, you give it your site address and it makes a request and checks what headers are included in the response. It then gives you a score and tells you which headers you’re missing.

If we take a look at my personal website, stephenreescarter.net, we can see it scores a A+! 🤓

“A+” result for stephenreescarter.net

Since securinglaravel.com is powered by Substack, it’s limited to what headers they include and only gets a D… 😭

“D” result for securinglaravel.com

And since I know some of you will be wondering, laravel.com scores a D too2

“D” result for laravel.com

So now you know which headers you’re missing, let’s quickly run through what each header does, and point you in the right direction to get it enabled.

Content-Security-Policy

I’ve covered Content Security Policies before, so I recommend checking out that post. We’ll also touch on them next week, since (spoiler alert!) “Missing Content Security” Policies is #3 on the top 10!

Referrer-Policy

Controls what referrer information is sent when the user navigates from your site to a different site. The current browser default value is strict-origin-when-cross-origin, which is a fairly sane default for many apps.

You should consider changing this value if your app shouldn’t broadcast it’s domain, i.e. for internal tooling, or private or sensitive apps. You can also tweak it if you want to broadcast complete referrers.

Referrer information can leak sensitive paths or even query string parameters, so you need to be aware of what the setting is and what information it is sharing.

Go checkout the mdm docs to learn about the options.

Referrer-Policy: no-referrer
Referrer-Policy: no-referrer-when-downgrade
Referrer-Policy: origin
Referrer-Policy: origin-when-cross-origin
Referrer-Policy: same-origin
Referrer-Policy: strict-origin
Referrer-Policy: strict-origin-when-cross-origin
Referrer-Policy: unsafe-url

Permissions-Policy

This is a newer header that defines what browser features the app has permissions to use. The idea being that you can disable things like the webcam and microphone in the header, so if an attacker compromises your site and injects some javascript, it can’t turn on the victim’s camera and mic and record them. In this way it’s similar to a CSP - preventing future exploitation of an existing vulnerability.

The easiest way to get started is to use the generator over at permissionspolicy.com.

Permissions-Policy: <directive> <allowlist>

More information on the mdm web docs…

Strict-Transport-Security (HSTS)

Informs the browser to always use HTTPS when connecting to that domain in the future (up until the expiry date). This prevents Person In The Middle Downgrade attacks that revert the connection back to unencrypted HTTP.

If you domain doesn’t require HTTP3, then there isn’t much reason to not enable this on your domain. You can optionally enable it across all subdomains to cover your entire app, and add it to the preload list to avoid the Trust On First Use (TOFU) problem4.

Some newer extensions, such as .dev and .app are included on the preload list already, giving you HSTS out of the box.

Strict-Transport-Security: max-age=<expire-time>
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains; preload

More information on the mdm web docs…

X-Content-Type-Options

Prevents the browser from trying to guess the content type of response/file from it’s MIME type and instead trust the Content-Type header. This is important to prevent uploaded files from tricking the browser into doing things it isn’t supposed to do, such as downloading or executing files.

There is a single value, which you can safely enable in almost all use cases:

X-Content-Type-Options: nosniff

More information on the mdm web docs…

X-Frame-Options

Prevents your site from being embedded within a frame, either by blocking all frames or only allowing a site to embed itself. This directly prevent clickjacking attacks.

Unless you specifically need to allow frames from third parties, you can safely enable the header with one of these options:

X-Frame-Options: DENY       << block all frames
X-Frame-Options: SAMEORIGIN << allow site to embed itself

Note, it is no longer possible to allow other sites to embed yours through this header. This has been moved to the Content Security Policy header with its frame-ancestors directive.

More information on the mdm web docs…

Dishonourable Mention: X-XSS-Protection

This was a non-standard header implemented on some browsers to try and protect against simple XSS attacks. It never really worked well, and in specific cases it actually caused XSS vulnerabilities. It has since been removed, so there is no reason to implement it.


  1. The main exception to this rule is X-Frame-Options and Content-Security-Policy direct prevent clickjacking by controlling how your app can be embedded within frames.

  2. So if the Laravel team are reading this… you’ve got work to do. 😜

  3. There aren’t many use cases left that do.

  4. TOFU → Trust On First Use.
    The first request must be trusted, as it contains the HSTS header instructing the browser to require HTTPS. If the first request is intercepted, the HSTS header can be stripped and bypassed, removing it’s protection. The preload list avoids this by shipping the list of HSTS domains with the browser, so it will never make a HTTP request to a domain on the list.