Securing Next.js SPAs: Headers
CSP, CORS, CORB, TrustedTypes, Permissions Policy, HSTS and a ton of headaches.
Alright, lets talk security. Headers are an integral part of securing web apps, and this article will discuss how to set them in Next 15.
Let's split this endeavour into 4 sections:
- Basic Headers
- Varying Headers
- Trusted types
- HTTPS & HSTS
But, before we dive in, let's outline several resources that are integral for understanding what headers we need and what they do.
- Security Headers: use this to check what headers are present now, what needs improving. This website also has links to
- Scott's Blog: the best resource to learn about different headers and what they do.
- MDN: the place for official documentation regarding HTTP.
- Next.js: for the official recommendation by developers of Next.js.
Instead of spending time copy-pasting info from these resources, I would rather redirect you to the proper channels. With that said, let's start!
Basic Headers
By "basic" I refer to headers that do no vary based on request. That is, they are present on every request to the origin.
Go to Security Headers and check what Headers you've already got, and what you need to add. Here is a short list of what I implement on mine:
- Referrer-Policy
- X-Content-Type-Options
- Permissions-Policy
- Cross-Origin-Embedder-Policy
- Cross-Origin-Opener-Policy
- Cross-Origin-Resource-Policy
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
- Strict-Transport-Security
To implement these, we can leverage next.config.ts async headers()
function. Here is an example (you can check out the full next.config.ts used by this website here)
Notes:
Deprecated Headers
You may have noticed I do not list X-Frame-Options
and X-XSS-Protection
. The reason for that is these headers are deprecated and replaced by CSP.
X-Frame-Options
: Deprecated and replaced by CSP frame-ancestors, which is talked about later in the CSP section.
X-XSS-Protection
: This header can itself create XSS vulnerabilities in perfectly safe websites. Use CSP to allow for safer and tighter control if you do not plan to support legacy browsers.
Referrer-Policy
MDN: Referrer-Policy
I highly suggest reading A new security header: Referrer Policy and making an informed decision based on the information provided. In short, avoid
unsafe-url
origin
(instead usestrict-origin
)origin-when-cross-origin
(instead usestrict-origin-when-cross-origin
)
Here is a little diagram to help you decide ;)
COEP, COOP, CORP
Once again, read this awesome article COEP COOP CORP CORS CORB - CRAP that's a lot of new stuff!. It explains this trio far better than I ever will.
Varying Headers
By "Varying" I mean the headers that will change depending on the request. Access-Control-Allow-Origin
is a prime example, as you do not want to expose all the whitelisted origins to all requests, only to those who actually come from a whitelisted origin.
To achieve this, we can use Next middleware.
CORS
This setup is copied directly from the official docs.
Quick rundown: check if the request origin is in our whitelist of origins. If it is, set Access-Control-Allow-Origin
with the value of that origin in the response. If its not, don't add anything, effectively prohibiting cross-origin requests.
Content Security Policy
MDN: Content Security Policy For general understanding of CSP: https://scotthelme.co.uk/csp-cheat-sheet For the Next.js way of implementing it: Configuring: Content Security Policy
In a nutshell, Content Security Policy controls what, who, and where can
- Make requests
- Execute JS
- Change CSS
We can set it up like so:
You can notice we only add the policy in production, as it usually breaks stuff development.
You can then use
to read the nonce and use it in your components.
Trusted Types
MDN: Trusted Types
The Trusted Types API helps in preventing DOM-based XSS attacks. It allows you to lock down the creation of DOM elements that accept HTML, URLs, or JavaScript code, making sure only "trusted" sources can inject or modify the DOM. With Trusted Types, you can avoid dangerous code like:
The default way to enable trusted types in React is to use dompurify - npm. The problem is, it breaks when using Next.js SSR. Luckily, isomorphic-dompurify - npm solves this problem.
We can add write a policy in any script, and then import it in layout.tsx
. Here is an example that allows json-ld
structured data scripts.
To enable trusted types, we have to add
to the CSP policy. The trusted-types
specifies which policies are allowed:
- default: our created policy, and the default policy used by our page.
- dompurify: policy created by dompurify
- nextjs#bundler: policy by next.js bundler
HTTPS and HSTS
HTTPS ensures that all communication between your users and your website is encrypted and secure. But encryption alone is not enough; attackers can still attempt to downgrade your connections. That's where HSTS comes into play, by ensuring that browsers only communicate with your site using HTTPS, even if the user or attacker tries to use an unencrypted HTTP connection.
Setting up HSTS in Next.js
To enforce HSTS, you'll need to add the Strict-Transport-Security
header. Here's an example configuration to include this header in your Next.js app.
max-age=31536000
: This tells the browser to remember that this site uses HTTPS for the next year (31,536,000 seconds).preload
: This allows the domain to be preloaded into browsers that support HSTS preload lists. You can submit your domain to https://hstspreload.org/ to be added to the preload list.includeSubDomains
: This ensures that HSTS is enforced for all subdomains.
Be careful when applying HSTS to subdomains. If you enable HSTS with includeSubDomains
on a top-level domain, all subdomains must support HTTPS.