Securing your static website with HTTP response headers

In this post, I'm going to go into how I secured my 11ty blog (this site), using Cloudflare pages and HTTP Response headers.

If you aren't bothered about all the information below, you can just jump straight to the code.

What is a response header?

An HTTP response header is a part of the response sent from a web server to a client (like your browser) when the client requests a resource, such as a webpage, image, or data.

The response header will contain additional information (metadata) about the request being made. Response headers come in three main categories:

  1. Information about the response, for example the status code of the resource being requested. e.g. 200 for an OK response or a status of 404 for not found.
  2. Details about the resource, for example a resource's Content Type e.g. text/html, text/css, application/javascript.
  3. Instructions for the client, for example how long to store a resource in a browser's cache (caching rules).

†: I realise that technically Content-Type isn't actually a response header, it's actually a representation header, which provides metadata about the message body, such as its length, type, and encoding, helping the recipient understand and process the data correctly. But for simplicity's sake we'll consider it to be a response header in this post.

For the rest of the blog post, we are going to focus on number 3: "Instructions for the client". As with the security HTTP response headers, we will be instructing a user's browser as to what it can, and can't do with resources on a webpage.

Why? It's a static website!

Now I know what some of you will be thinking, since it's just static HTML and only hosted on a basic web server (with nothing dynamic), surely, there's nothing to secure? I mean, it's not like a hacker can hack into the Content Management System (CMS), and steal a username and password because in most cases there isn't one! This is true, but it's important to remember, static websites can be used for anything. I'm sure plenty of developers use static websites to build transactional websites that accept sensitive user data, like their address, Social Security Number (US), National Insurance Number (UK) or Credit Card details. So really, the security you are implementing isn't to protect you, it's to protect your users.

Here is a list of reasons below:

  1. Preventing Cross-Site Scripting (XSS) Attacks
  2. Securing Sensitive User Data
  3. Reducing Information Leakage
  4. Isolating Contexts
  5. Blocking Unintended Features
  6. Preventing Clickjacking
  7. Improving Browser Behaviour
  8. Enhancing Trust and SEO
    • A secure website builds trust with users and browsers. Search engines also rank secure websites, potentially improving your site's SEO rankings.

It's important to remember that even static websites can serve as entry points for attackers, host phishing pages, or be used in more complex attack chains.

Just show me the code

It's worth mentioning that the examples I'm giving aren't specific to Cloudflare Pages or 11ty. They can be used with any static website, or indeed any website or web server (e.g. Apache, Nginx, IIS, Tomcat, Node.js).

The code below sits in my _headers file in the public assets directory of my 11ty blog on Cloudflare Pages. There's a tonne of information in the Cloudflare documentation on headers here if you're interested.

/*
  Access-Control-Allow-Origin: https://nooshu.com
  Cache-Control: public, s-maxage=31536000, max-age=31536000
  Content-Security-Policy: base-uri 'self';child-src 'self';connect-src 'self';default-src 'none';img-src 'self' https://v1.indieweb-avatar.11ty.dev/;font-src 'self';form-action 'self' https://webmention.io https://submit-form.com/DmOc8anHq;frame-ancestors;frame-src 'self' https://player.vimeo.com/ https://www.slideshare.net/ https://www.youtube.com/ https://giscus.app/ https://www.google.com/;manifest-src 'self';media-src 'self';object-src 'none';script-src 'self' https://giscus.app/ https://www.google.com/ https://www.gstatic.com/;style-src 'self' 'unsafe-inline' https://giscus.app/;worker-src 'self';upgrade-insecure-requests;
  Cross-Origin-Opener-Policy: same-origin
  Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
  Referrer-Policy: strict-origin-when-cross-origin
  Cross-Origin-Resource-Policy: cross-origin
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  X-Content-Type-Options: nosniff
  X-DNS-Prefetch-Control: off
  X-Frame-Options: DENY
  X-Permitted-Cross-Domain-Policies: none
  Origin-Agent-Cluster: ?1

Or I've created a GitHub Gist here if that's easier to copy, paste, and modify.

Just remember to change the Content-Security-Policy (CSP) and ensure it allows any 3rd-party assets you load on your website. For this, you are specifically looking at the following CSP directives:

  • form-action for form actions to a third party, e.g. a contact form.
  • script-src to allow the execution of scripts from a third party, e.g. Google Analytics.
  • img-src to allow images to load from a third party, e.g. Webmention images.
  • frame-src to allow <iframe> embeds to load on your website e.g. YouTube, Vimeo, Twitter

Other Static website hosting

I've also created Gists for other static website hosts below, if you don't use Cloudflare pages.

Important: These versions haven't been tested, so use them at your own risk! They are a direct port from my Cloudflare Pages _headers file to whatever format the specific host uses. Please do contact me if you find any errors in any of these versions and I will update the Gist and attribute the change to you in the post changelog.

Netlify

As a _headers file or a netlify.toml file.

Vercel

As a vercel.json file.

GitHub Pages

GitHub Pages itself does not support custom headers directly. Use a custom domain with a proxy service like Cloudflare to inject headers.

Cloudflare Pages

Repeating myself I know, but for anyone who just quick scans looking for a Cloudflare Pages version it is here.

Surge

A Surge version can be found here.

Render

A Render version can be found here.

Firebase Hosting

A Firebase version can be found here.

Heroku

A Heroku version can be found here.

InfinityFree

An InfinityFree version can be found here.

DigitalOcean

  • A complete nginx.conf config for DigitalOcean version can be found here.
  • Or if you only require the location block for Nginx that can be found here.

1&1 IONOS

1&1 IONOS itself does not support custom headers directly. Use a custom domain with a proxy service like Cloudflare to inject headers.

Header Specifics

To learn what each of these response headers does, continue reading below:

Access-Control-Allow-Origin

Description

This header specifies which origins (domains) are permitted to access the resources on the server via cross-origin requests. It is primarily used in handling Cross-Origin Resource Sharing (CORS).

Example

Access-Control-Allow-Origin: *

Recommended value

Instead of using *, which allows any origin to access the resource (less secure), specify the trusted origin explicitly: Access-Control-Allow-Origin: https://yourdomain.com

Recommended value sources

Specification

W3C Specification.

Cache-Control

Description

The Cache-Control header doesn't have anything to do with security, but it's an essential header for web performance as it gives the browser instructions on how an asset is stored or cached in the browser.

Example

Cache-Control: public, max-age=31536000, immutable

Recommended value

There are too many options for this particular header. If you are interested in learning more about efficient browser caching, I recommend checking out the following sources.

Recommended value sources

Specification

W3C Specification.

Content-Security-Policy (CSP)

Description

The Content Security Policy (CSP) header is a security feature that protects web applications from attacks like Cross-Site Scripting (XSS) and code injection by controlling the sources from which browsers can load and execute content. If there's only a single response header you implement today, then make it the CSP Response header.

Example

Here's a basic example of a CSP, I recommend checking out the resources below to ensure your website is secured with a comprehensive Content Security Policy. Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;

Recommended value

There are so many directives and settings available for a CSP, it's impossible to define a one-size fits all value. The best approach is to ensure you have one for your website. Adding a CSP to an existing website can be complicated, luckily there are tools out there to help developers, a number of which are listed below.

Recommended value sources

Specification

W3C Recommendation — CSP2.

Cross-Origin-Opener-Policy (COOP)

Description

The COOP header controls whether a document can share its browsing context group with other documents from different origins. It helps isolate your web page from potentially malicious cross-origin interactions by preventing the sharing of resources like global objects and browsing contexts.

Example

Cross-Origin-Opener-Policy: unsafe-none

Recommended value

The recommended value isolates the page entirely from other origins. Cross-Origin-Opener-Policy: same-origin

Recommended value sources

Specification

HTML Living Standard.

Permissions-Policy (formerly Feature-Policy)

Description

Enables developers to specify which browser features can be accessed by different origins, both on the main page and within embedded frames.

Example

Permissions-Policy: geolocation=(), microphone=()

Recommended value

This isn't a one size fits all header, you basically disable any browsers features that you know your site will never use. A list of valid permission directives can be found here. In the above example we are disabling the geolocation and microphone browser functionality on all origins.

Recommended value sources

Specification

W3C Editor's Draft.

Referrer-Policy

Description

The Referrer-Policy HTTP response header determines the amount of referrer data to include in the Referrer header for outgoing requests. This security policy can either be set as a response header or in the <head> of the HTML document.

Example

Referrer-Policy: origin OR <meta name="referrer" content="origin" />

Recommended value

Referrer-Policy: strict-origin-when-cross-origin OR <meta name="referrer" content="strict-origin-when-cross-origin" />

Recommended value sources

Specification

W3C's Editors Draft.

Cross-Origin-Resource-Policy (CORP)

Description

The Cross-Origin-Resource-Policy (CORP) header is a security feature that controls whether resources like images, scripts, or fonts can be accessed by other websites to prevent unauthorized cross-origin access.

Example

Cross-Origin-Resource-Policy: same-origin

Recommended value

Cross-Origin-Resource-Policy: same-site

Recommended value sources

Specification

Fetch Living Standard.

Cross-Origin-Embedder-Policy (COEP)

Description

The Cross-Origin-Embedder-Policy (COEP) header is a security feature that ensures your webpage only embeds safe, explicitly authorized resources, protecting against vulnerabilities like data leaks or Spectre attacks. It is essential for sites using advanced web features like SharedArrayBuffer or WebAssembly to enhance security and performance.

Important: I've included this header for completeness, but I currently don't have this header enabled on this site. This is because the comment technology I use Giscus, doesn't support the COEP header. I have raised an issue on their GitHub Repository here, but unfortunately, there's yet to be any comments as to when it will be supported. So be careful with this header if you embed <iframe>'s into your site as this header may break the functionality.

Example

Cross-Origin-Embedder-Policy: unsafe-none

Recommended value

Cross-Origin-Embedder-Policy: require-corp

Recommended value sources

Specification

HTML Living Standard.

Strict-Transport-Security

Description

The Strict-Transport-Security (HSTS) header is a security feature that forces web browsers to communicate only over HTTPS with a website, ensuring that all subsequent requests are secure. It prevents attackers from exploiting protocols like HTTP or performing downgrade attacks. When enabled, the header includes parameters such as max-age (the duration for which the browser should enforce HTTPS), includeSubDomains (to apply to all subdomains), and preload (to include the site in browsers' HSTS preload lists). This header significantly enhances security by protecting users from man-in-the-middle (MITM) attacks and ensuring encrypted communication.

Example

Strict-Transport-Security: max-age=86400

Recommended value

The max-age recommendation of 2-years seems to be quite a contentious topic as Google's preload list recommends 1-year (31536000), whereas OWASP recommends 2-years. There were varied opinions when I asked about it earlier in the year on the Cloudflare forums. I currently have it set to 2-years, so it can be added to Google's Preload list and conforms with OWASP's recommendation (links in the sources below). Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

Recommended value sources

Specification

HTTP Strict Transport Security (HSTS).

X-Content-Type-Options

Description

The X-Content-Type-Options header is a security feature that prevents browsers from interpreting files as a different MIME type than what is specified by the server. By setting it to nosniff, it ensures that browsers strictly adhere to the declared Content-Type, reducing the risk of MIME type confusion attacks, such as executing malicious scripts disguised as other file types. This header is particularly effective in preventing cross-site scripting (XSS) and drive-by download attacks.

Example

X-Content-Type-Options: none

Recommended value

There's only a single directive with this header, but I've linked to additional resources below for further reference. X-Content-Type-Options: nosniff

Recommended value sources

Specification

Fetch Living Standard.

X-DNS-Prefetch-Control

Description

The X-DNS-Prefetch-Control header controls whether browsers should perform DNS prefetching, a performance feature that resolves domain names of links on a page before they are clicked. By setting it to on, DNS prefetching is enabled, potentially improving page load times. Conversely, setting it to off disables this behaviour, which can enhance privacy and security by preventing unnecessary DNS queries that could expose browsing behaviour or enable certain tracking methods. This header is useful for balancing performance and privacy needs.

Important: This header is currently a non-standard header, so check browser support before using.

Example

X-DNS-Prefetch-Control: on

Recommended value

X-DNS-Prefetch-Control: off

Recommended value sources

Specification

Not part of any current specification at the time of writing.

X-Frame-Options

Description

The X-Frame-Options HTTP header prevents clickjacking attacks by controlling whether a web page can be embedded in a frame or iframe. It supports three values: DENY (disallows framing completely), SAMEORIGIN (allows framing only on the same origin), and ALLOW-FROM URL (deprecated, allows framing from a specific origin). This header is being replaced by the more flexible frame-ancestors directive in Content Security Policy (CSP).

Important: This header is gradually being replaced with the (CSP) frame-ancestors directive, but implementing both while this crossover happens should cause no negative impact to your website.

Example

The ALLOW-FROM option is not widely supported and is largely replaced by the frame-ancestors directive in CSP. X-Frame-Options: ALLOW-FROM https://example.com

Recommended value

X-Frame-Options: DENY

Recommended value sources

Specification

HTML Living Standard.

X-Permitted-Cross-Domain-Policies

Description

The X-Permitted-Cross-Domain-Policies header specifies whether a browser or client can load cross-domain content, such as Flash or PDF files, from your domain. By setting it to values like none, master-only, or by-content-type, it controls the loading of potentially unsafe resources, mitigating risks such as data theft or unauthorized content execution. This header is particularly relevant for applications using plugins like Adobe Flash, providing a safeguard against cross-origin attacks and unauthorized data access.

Important: This header is currently a non-standard header, so check browser support before using.

Example

X-Permitted-Cross-Domain-Policies: by-content-type

Recommended value

X-Permitted-Cross-Domain-Policies: none

Recommended value sources

Specification

Origin-Agent-Cluster

Description

The Origin-Agent-Cluster header controls whether a webpage should have its own separate "agent cluster", isolating it from other origins in terms of memory and execution contexts. By setting it to ?1, the header ensures that the page's scripts, documents, and workers operate in isolation, preventing data leakage and improving security by mitigating cross-origin side-channel attacks like Spectre. This isolation enhances security for web applications, especially in environments with sensitive data or shared hosting scenarios.

Important: This header is currently a non-standard header, so check browser support before using.

Example

Origin-Agent-Cluster: ?1

Recommended value

The value of ?1 is the structured header syntax for a boolean true value. Origin-Agent-Cluster: ?1

Recommended value sources

Specification

HTML Living Standard.

Final results

So let's have a quick look at the final results of improving the HTTP Security headers on this blog by running it through the security scanner Security Headers by Snyk.

Before

The score before came out at a pretty poor "D":Nooshu.com with a security score of D on the Security Headers scanner.

After

The score after implementing the above changes comes out at a much better "A+"!:Nooshu.com with a security score of A+ on the Security Headers scanner.

In summary

In this post, we've looked at what a response header is, why security response headers are important (even for static websites!), what the different implementations look like on different static hosting platforms, and also details on each of the response headers currently in use on this blog. Thanks for reading! I hope you found it interesting! And as always, if you spot any errors or have any feedback, please do let me know!


Post changelog:

  • 28/12/24: Initial post published.
  • 29/12/24: Added the HTML5 Boilerplate Apache Permissions Policy example (Thanks to Don Marti).

Webmentions

Mentioned on:

  1. https://blog.zgp.org/security-headers-for-a-static-site/
  2. https://blog.zgp.org/security-headers-for-a-static-site/

A webmention is a way to notify this site when you've linked to it from your own site.