HTTP Security Headers: The Best Practices
Initially used for simple metadata, HTTP headers now play an important role in the vast field that web security is.
Setting up HTTP security headers is the quickest, less expensive, and probably the most effective way to secure a web application today. Here is how.
But first, you may be wondering how to quickly inspect the HTTP headers of a website? curl
of course!
$ curl -I https://kerkour.com
HTTP/2 200
date: Thu, 29 Jul 2021 08:56:43 GMT
content-type: text/html; charset=UTF-8
etag: W/"a143c521da9f559fefdb236fb2a1e842-ssl-df"
permissions-policy: interest-cohort=()
strict-transport-security: max-age=15552000; includeSubDomains
vary: Accept-Encoding
x-xss-protection: 1; mode=block
age: 665
cache-control: public,max-age=0,s-maxage=31536000
x-content-type-options: nosniff
x-download-options: noopen
x-frame-options: Deny
x-nf-request-id: 01FBRQ1J6CNSET6W9JTFJ37GD4
cf-cache-status: HIT
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
server: cloudflare
cf-ray: 676522b649b3086f-CDG
alt-svc: h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400
Content-Security-Policy
The Content-Security-Policy
header (also known as CSP) is the most important one but also the most tedious to configure.
It does not only provide precise control over the resources that can be loaded on your website but also a way to detect and monitor infringements of the rules, and thus potential hacking attempts.
When creating a CSP, you actually create an allowlist of which resources can be loaded from your website and which servers can be reached. It can, among other things, prevent the execution of an XSS attack or prevent the exfiltration of data from a compromised dependency.
Here is an example of CSP for a Vue.js Single Page Application backed by an API and which stores uploads in an S3 bucket (a typical SaaS, you need to change https://myreporturi.com
):
Content-Security-Policy: default-src 'self' https://js.stripe.com; img-src 'self' data:; script-src 'self' 'unsafe-eval' https://js.stripe.com; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' https://bucket.s3.eu-west-1.amazonaws.com; report-uri https://myreporturi.com
Here is the same CSP, but splitted into multiple line for better readability (and thus is invalid):
Content-Security-Policy: default-src 'self' https://js.stripe.com;
img-src 'self' data:;
script-src 'self' 'unsafe-eval' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
object-src 'none';
connect-src 'self' https://bucket.s3.eu-west-1.amazonaws.com;
report-uri https://myreporturi.com
You can see that this CSP is not perfect (unsafe-eval
, unsafe-inline
are present) but still protects against a lot of attack vectors. Also note that this CSP is configured to report violations to https://myreporturi.com
, which may help you detect attacks or misconfigured resource sources. You can use an open source solution such as Sentry which provides a turnkey solution for CSP violations monitoring.
CSP is for sure a little bit hard to get it right, especially for content-heavy websites, but it's, in my opinion, the easiest to secure a website against most client-side injections attacks and data exfiltration!
You can learn more about it here.
You can also use this free tool to generate a CSP for your website.
Strict-Transport-Security
Also known as HSTS (for HTTP Strict Transport Security) tells browsers to load your website only over HTTPS. You can, among other things, apply this policy to all the subdomains.
Here are good values to secure your website:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
You can learn more about it here.
X-Content-Type-Options
If an attacker succeeds at uploading a malicious file, for example, masquerading an HTML file as an image with a .png
extension, and the file is then served without a Content-Type
header, the browser may actually recognize the file as being an HTML file and potentially execute scripts inside it. This is known as content sniffing: the automatic detection of the content type. To secure your website, you need to tell browsers to NOT sniff content.
Here is how:
X-Content-Type-Options: nosniff
You can learn more about it here.
X-Frame-Options
Some attacks such as XSS or phishing may embed your website in an iframe
. If you have no reason to embed your website in an iframe
(and most of the time you shouldn't), you have to block this risk with the X-Frame-Options
header, which is used to indicate whether or not a browser can render a page in a <frame>
, <iframe>
, <embed>
or <object>
.
Here is what you need most of the time:
X-Frame-Options: Deny
You can learn more about it here.
X-XSS-Protection
The X-XSS-Protection
header tells browsers to stop loading pages if an XSS attack is detected.
This header is today deprecated (too many false positives, bugs, and was judged ineficient), you don't need to configure this header. However, if you insist, or need to do it for compliance reasons, you can set it to this value:
X-XSS-Protection: 0
You can learn more about it here, and the reasons of deprecation on the OWASP project's dedicated page.
Expect-CT
And last but not least, the Expect-CT
header tells browsers to opt in Certificate Transparency reporting and/or enforcement to, among other things, help the detection of malicious HTTPS certificates by rogue or compromised certificate authorities.
Here is a good example of value (you need to change https://myreporturi.com
):
Expect-CT: max-age=86400, enforce, report-uri=https://myreporturi.com
Sentry also provides a solution for Expect-CT
failures monitoring.
You can learn more about it here.
Conclusion
Other than for Content-Security-Policy
, setting up those HTTP security headers shouldn't take you more than 2 minutes. Do it now!
Here are a few links to get started configuring HTTP headers on the most common web servers/frameworks: