How to add HTTP headers to a Quarto blog on Netlify

Quarto is a great blog framework but its documentation can be a bit thin. Here’s how we added HTTP headers to this blog to prevent click-jacking.

Apricot Tech
Author

Brian Kent

Published

June 16, 2023

A little break today from the usual Apricot fare.

I recently learned about the security risk of click-jacking, which is a way to embed somebody else’s web content under a transparent overlay to trick users into doing things they shouldn’t. Somebody could embed the content on this blog, for example, on another website with transparent links overlaid that download malicious code.

To be honest, I don’t know how much of a risk this really is, but the 80% solution is super easy (in theory) and doesn’t seem like it can hurt.

Warning

I am not a security expert and this post is not about security; please do not treat it as a reference for security best-practices.

A solution, in theory

One way to prevent your web content from being embedded on another site in a frame specifically is to include the X-Frame-Options header in the HTTP response, with the policy set to DENY or SAMEORIGIN. I prefer DENY because I don’t think I’ll ever need to embed my own blog content, but it seems like SAMEORIGIN is more common.

OK, simple enough, but how to make this happen?

A fly in the ointment

This blog is built with the Quarto framework and published on Netlify.

Let’s start with Netlify, because that’s there the HTTP response header is ultimately going to be served from. Netlify’s Custom Headers page says we just need to include a file _headers in our publish directory and even gives an example for the clickjacking problem:

/*
  X-Frame-Options: DENY
  X-XSS-Protection: 1; mode=block

OK, now back to Quarto, where we find the fly in the ointment.1 We need to get this file into our publish directory (by default this is called _site). But we can’t just put the file into the _site directory because Quarto deletes it every time it renders the source.

The Quarto docs do have a page about headers, but they’re HTML headers, not HTTP, so that’s no help.

The solution, for real

Instead, we need to refer to Quarto’s site resources page, which shows how to include files like _headers (or other things like images) in the _site directory after rendering.

In the root directory of our project we have a config file called _quarto.yml. In the project field of that file we list _headers as a resource, which tells Quarto to copy the _headers file over every time it renders.

project:
  type: website
  resources:
   - "_headers"

So just leave the _headers file in the root directory. Render the changes and publish to Netlify and we’re good to go.

$ quarto render
$ quarto publish --no-render netlify

To verify, open the dev tools in your favorite browser and go to the network tab. Open (or refresh) your blog and click on the top entry, which should be the page itself (not a secondary resource). To the right, you should see a section for Response Headers, and you should see the new X-Frame-Options entry.

Confirmed, there’s the X-Frame-Options header, toward the bottom of the image, set to DENY attempts to embed this blog’s content in any frame.

Footnotes

  1. I love Quarto and can’t recommend it enough for technical writing of almost any kind, but for web and blog applications the documentation can be a bit thin.↩︎