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?
#1 → Exposed API Keys & Passwords
#2 → Missing Authorisation
#3 → Missing Content Security Policy (CSP)
#4 → Missing Security Headers
#5 → Insecure Function Use
#6 → Outdated & Vulnerable Dependencies
#7 → Cross-Site Scripting (XSS)
#8 → Insufficient Rate Limiting
#9 → Missing Subresource Integrity (SRI)
#10 → Insufficient Input Validation & Mass-Assignment Vulnerabilities
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 - 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+! 🤓
Since securinglaravel.com is powered by Substack, it’s limited to what headers they include and only gets a D… 😭
Now on Ghost, it scores a pitiful F... 😱 I need to figure out how to add some headers!
And since I know some of you will be wondering, laravel.com scores a D too!
So if anyone the Laravel team are reading this… you’ve got work to do! 😜
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.
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>
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 HTTP, 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) problem.
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.
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
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.
If you've made it this far, please share this Security Tip and Securing Laravel with your Laravel friends, colleagues, and enemies! The more folks we have learning about security, the more secure code will be written, and the safer the whole community and our users will be. Also, if you tag me I'll give it a retweet, and you can find all my socials on Pinkary.