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-Policyheader 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_headersfor 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!