How I achived an A+ on all security tests using AstroJS
- Léo Mercier @Sawangg
Astro being my new frontend framework of choice to avoid the triangle company, I went on a quest to achieve maximum
security for my users without sacrificing on developer experience. I achieved 99% of what I set out to do and in this
article, I’ll present you the solutions I found along the way.
Security Headers
The first test I wanted to pass was the Mozilla observatory. The solution will depend on two factors. First, if your site is static or dynamic and second on your hosting. My website is fully dynamic and hosted on the edge network of Cloudflare. So, I created a middleware that added all the necessary headers.
const securityHeadersMiddleware = defineMiddleware(async (_, next) => {
const response = await next();
if (response.headers.get("content-type") !== "text/html") return response;
// NOTE: The nonce should not be created and replaced here, it defeats the purpose of the CSP.
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = `
default-src 'none';
script-src 'strict-dynamic' 'nonce-${nonce}';
style-src 'self' 'unsafe-inline';
img-src 'self' https: blob: data:;
font-src 'self';
form-action 'self';
frame-ancestors 'none';
base-uri 'none';
connect-src 'self';
upgrade-insecure-requests;
`;
const body = (await response.text()).replaceAll("<script", `<script nonce="${nonce}"`);
response.headers.set("Access-Control-Allow-Origin", import.meta.env.PROD ? import.meta.env.SITE : "*");
response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Referrer-Policy", "strict-origin");
response.headers.set(
"Permissions-Policy",
"accelerometer=(), autoplay=(self), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(self), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), xr-spatial-tracking=()",
);
response.headers.set("Cross-Origin-Embedder-Policy", "require-corp");
response.headers.set("Cross-Origin-Opener-Policy", "same-origin");
response.headers.set("Cross-Origin-Resource-Policy", "same-origin");
response.headers.set("Content-Security-Policy", cspHeader.replace(/\s{2,}/g, " ").trim());
response.headers.set("X-Nonce", nonce);
return new Response(body, response);
});
As you can see, there is a workaround to make the Content-Security-Policy
header work. We generate the nonce
inside
the middleware and simply replace the script
tags with the nonce
. This is not secure but there is currently
no better way. An integration called @kindspells/astro-shield
could work but I haven’t tested it myself.
The values of the different headers will depend on what you want to achieve, my middleware is really strict. You can find more informations on the security headers website as well as the Mozilla web security page.
If you’re on a static website, you could add
<meta>
tags in your<head>
to achieve the same result, except theContent-Security-Policy
header that would require a bit more work. You could also use your hosting provider to set the headers either with rules of with custom files such as_headers
for Netlify and Cloudflare.
Cloudflare
If you’re using Cloudflare workers like myself, the first thing you’ll need to do is disable the feature called Speed Brain
inside your Cloudflare dashboard. This features injects scripts in your head
tag for prefetching, resulting in
a CSP error. Prefetching is done inside Astro if you configured it, so you don’t need this feature.
You might have noticed I’m using the Crypto API
in the middleware. This means that if you plan on using it,
you’ll need to enable the nodejs_compat
feature of your worker and use a minimum compatibility date of
2024-09-23
. Here is an example wrangler.toml
in the root of my project.
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
Security.txt
A new standard as emerged to disclose security vulnerabilities to website owners. The
security.txt file, located in the .well-known/
folder aims to create a simple way for
security researchers to contact you. You can simply create this file in Astro by adding .well-known/security.txt
to
your public
folder.
Next steps
The Subresource Integrity will be the next subject I’ll tackle inside Astro. I’d also like to remove the unsafe-inline
for the style-src
directive of my Content Security Policy.
Thank you for reading and see you soon!