Understanding and implementing a proper Content Security Policy with Next.js and Styled Components
Rees Morris ยท 23 Apr, 2021
Update Jan 2022: Due to the popularity this article seems to have gathered, I've re-written huge parts of it to make it even more informative.
Introduction
This post aims to discuss the best ways to implement a secure and scalable Content Security Policy on a website, along with some theory as to just what a Content Security Policy (CSP) actually is. We'll be using Next.js and styled-components for the code here, though the theory should be easily transferable to any other library.
Depending on the architecture of your site, you may not be able to implement a wholly complete Content Security Policy (eg. certain aspects require you to be running a web server). As such, the first few sections of this post should apply to virtually any website, and it's strongly encouraged to have at least a baseline policy in place.
Before we begin, I must emphasise that I am not a security expert; I'm just a frontend developer who was tasked with implementing this a few months back and am sharing everything I learned here. The ideas and suggestions below are things found on my own journey using the scarce amount of internet resources on the topic, so don't assume this is the only / the best way to approach CSP!
Content Security What?
The purpose of the Content Security Policy is to add a layer of protection between a website and browser by blocking specific directives that don't align with your policy.
In short, CSP essentially forces you to 'whitelist' the origins that resources on your site (such as scripts, styles, fonts, images, etc.) can load from. If a resource is being requested from an origin that isn't on the whitelist, it will be blocked by the browser. This makes CSP an excellent layer of defence against XSS and data injection attacks.
CSP is typically added to a site in one of two ways: either as a HTTP header returned by the web server, or as a meta tag element embedded within the head. Whilst the HTTP header is more common, it mostly depends on your setup: some applications (such as completely static sites) don't have any server-side rendering, so will need to rely on the meta tag instead. Both are just as valid!
CSP Syntax
The syntax of a Content Security Policy looks like the following:
Content-Security-Policy: <directive> <values>; <directive> <values>;
<meta
http-equiv="Content-Security-Policy"
content="<directive> <values>; <directive> <values>"
/>
Within the syntax, we have directives and values. The directive is declared first, and then its value(s) afterwards. A directive can have any number of values, each separated by a space.
We can declare as many directives as we need, each separated by a semicolon after the last value of the previous directive.
-
The directives relate to the different resources on our site (scripts, styles, fonts, images, etc.). For example, to specify where our images can be loaded from, we would use
img-src
. -
The values tell the browser which origins are allowed for that directive. Each value is separated by a space. This typically consists of a URI string, but can also include predefined keywords, which must be wrapped in
'single quotes'
.
Let's look at the Twitter website as an example. They use the HTTP response header method of setting a policy, and a cherry-picked snippet of it looks like this:
content-security-policy:
font-src 'self' https://*.twimg.com;
img-src 'self' blob: data: https://*.cdn.twitter.com https://ton.twitter.com;
object-src 'none';
script-src 'self' 'nonce-ZDZiNGQyMjMtYTQ4YS00MmUyLWJiZjUtNzI0MjA0OTdiMzYy';
default-src 'self';
- the
font-src
directive is using the'self'
keyword; which allows fonts hosted on the same origin to be loaded, as well as fonts from any subdomain ontwimg.com
- the
img-src
directive also uses the'self'
keyword; allowing images hosted on the same origin to load, as well as anyblob:
anddata:
URIs (arguably insecure), and any images loaded on a subdomain ofcdn.twitter.com
or onton.twitter.com
- the
object-src
directive is using the'none'
keyword which means that anything using this directive will be blocked; there is no allowed implementation - the
script-src
directive also uses the'self'
keyword, allowing scripts hosted on the same origin to load, as well as thenonce-
keyword, which holds a random, cryptographically secure token that can be attached to inline elements (scripts, in this case); we'll learn much more about this later - the
default-src
directive is also set toself
; we'll learn more about this in just a moment
It may be useful to know that the mapping of elements to directives is handled by the browser: if we add an <img src="" />
element to our site, the browser will always validate the src
value against the img-src
directive; we can't tell the browser to use something different.
Starting from the bottom
Arguably one of the most important CSP directives is the default-src
directive. This directive acts as the fallback if a specific directive is needed, but missing.
For instance, looking at the Twitter example above, let's say we add an iframe to our site. The browser would first check for a frame-src
directive to compare the iframe's src
value to. In the event the directive doesn't exist, the browser will fall back to the default-src
directive instead, and will follow whatever rules are set there. In this case, our iframe would only load if the src
was the same origin, due to the default-src 'self'
declaration.
This can be a blessing and a curse. Whilst it will save us time in the long run from having to declare dozens of different directives which all just use the 'self'
keyword, it also opens up a vulnerability for our site: every undeclared directive will indirectly behave based on our default-src
directive's values.
As such, the Mozilla Observatory and CSP Quick Reference Guide both recommend setting default-src
to 'none'
and expanding other specific directives outwards when needed. With this mindset, if a directive isn't explicitly declared, it's blocked. It makes the site much more secure, and debugging a lot easier.
Implementing our CSP
Let's add a Content Security Policy to our site.
As above, we'll start with just a simple default-src 'none'
declaration within a meta tag, and then begin expanding outwards until our site returns to its expected behaviour.
You can either add the tag to a custom _document file, or an existing component that imports next/head (such as an SEO component).
Be sure to place it above all other tags and scripts.
/* ... */
render() {
return (
<Html>
<Head>
<meta
httpEquiv='Content-Security-Policy'
content="default-src 'none'"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
import React from 'react';
import NextHead from 'next/head';
const Head = () => {
return (
<NextHead>
<meta httpEquiv='Content-Security-Policy' content="default-src 'none'" />
</NextHead>
);
};
export default Head;
You'll of course need to include this in your _app
page (or any component that wraps all pages, like a custom Page
component).
When refreshing the site, you might notice that everything has broken. Let's see what's going on.
Breaking down CSP errors
Most browsers tend to provide extremely helpful information when it comes to why our elements are being blocked when a Content Security Policy is in place.
I'll be using the follow-along repository for this, so your order of errors may differ - though we'll be resolving them all anyway!
When looking at the browser console in Chrome, the first errors for me have all been collapsed into the following message:
Refused to load the script '<URL>' because it violates the following Content Security Policy directive: "default-src 'none'". Note that 'script-src-elem' was not explicitly set, so 'default-src' is used as a fallback.
From this message alone, we get some idea of what's happening. We know already that certain URLs have been blocked from executing, and we can see that they are part of the script-src-elem
directive. When expanding the list of errors, it looks like they all come from the local _next/static
directory - hosted within the same origin of our site (localhost).
We could whitelist our current origin (http://localhost:3000 in my case) along with our live domain (such as reesmorris.co.uk), but that can make things potentially unsafe if our domain ever changes, as well as a pain to manage if your site has regional domains. This is where the power of the self
keyword comes in. Instead of having to directly declare the domains the user will be visiting the site on, the 'self'
keyword simply represents just that: the current origin!
Following the error message in the browser, we know we just need to declare a script-src-elem
directive with the 'self'
keyword value to fix this error. Let's update our policy just as such:
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'none'; script-src-elem 'self'"
/>
Reloading the page doesn't give us much more to see, but the errors have changed! This is because those scripts are now being allowed to run, which can lead to other errors.
The next error to appear is:
Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "default-src 'none'".
This error is telling us that a script was prevented from running because it uses the eval()
function, which is blocked by default due to the high risk it poses.
This error isn't actually being thrown by your code (unless you do use the eval() function, in which case you should remove it and return here), but instead by the development environment of Next.js, which uses eval()
"to generate and rebuild [eval source maps] during development".
Let's add a script-src
directive with the unsafe-eval
keyword and see what changes:
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'none'; script-src-elem 'self'; script-src 'unsafe-eval'"
/>
Success(?)! The content of the page is now visible, but we've now got two problems to deal with. The first is that there seems to be a giant "Unhandled Runtime Error" message on the page, and the second is we've just added 'unsafe-eval'
to our Content Security Policy; which we definitely don't want in production.
Looking at the console, there's a whole bunch more errors now - since a lot more things are running and then being blocked. Ignoring all styled-components related errors for now, this one stands out:
Refused to connect to 'ws://localhost:3000/_next/webpack-hmr' because it violates the following Content Security Policy directive: "default-src 'none'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.
It looks like Webpack is requiring a connect-src
directive to be present. All of the collapsed URLs again point solely to our localhost origin, so we can again use the 'self'
keyword here:
<meta
httpEquiv="Content-Security-Policy"
content="default-src 'none'; script-src-elem 'self'; script-src 'unsafe-eval'; connect-src 'self'"
/>
Hooray! Our errors have gone, and all JavaScript and links on the development site are behaving as normal. We still don't have any styling, but we'll get to that after making our policy much safer.
Creating a CSP Generator function
We obviously don't want 'unsafe-eval'
to be present in our production build, and we can probably remove connect-src
in production too, since (in the follow-along example) only Webpack requires it. We could make a conditional that returns different strings, but that will make this extremely difficult to maintain.
Instead, let's make a function that 'generates' our CSP for us!
// utils/generate-csp.js
const generateCSP = () => {
const policy = {};
// adder function for our policy object
const add = (directive, value, options = {}) => {
if (options.devOnly && process.env.NODE_ENV !== 'development') return;
const curr = policy[directive];
policy[directive] = curr ? [...curr, value] : [value];
};
// default-src
add('default-src', `'none'`);
// script-src-elem
add('script-src-elem', `'self'`);
// script-src
add('script-src', `'unsafe-eval'`, { devOnly: true });
// connect-src
add('connect-src', `'self'`, { devOnly: true });
// return the object in a formatted value (this won't work on IE11 without a polyfill!)
return Object.entries(policy)
.map(([key, value]) => `${key} ${value.join(' ')}`)
.join('; ');
};
export default generateCSP;
// utils/generate-csp.ts
type Directive = ''; /* See the Gist link below */
type Value = string;
interface Options {
devOnly?: boolean;
}
const generateCSP = () => {
const policy: Partial<Record<Directive, Value[]>> = {};
// adder function for our policy object
const add = (directive: Directive, value: Value, options: Options = {}) => {
if (options.devOnly && process.env.NODE_ENV !== 'development') return;
const curr = policy[directive];
policy[directive] = curr ? [...curr, value] : [value];
};
// default-src
add('default-src', `'none'`);
// script-src-elem
add('script-src-elem', `'self'`);
// script-src
add('script-src', `'unsafe-eval'`, { devOnly: true });
// connect-src
add('connect-src', `'self'`, { devOnly: true });
// return the object in a formatted value (this won't work on IE11 without a polyfill!)
return Object.entries(policy)
.map(([key, value]) => `${key} ${value.join(' ')}`)
.join('; ');
};
export default generateCSP;
I've included the Directive
types in this Gist.
This is my personal preference for writing CSP declarations, but you're certainly free to modify this to your own style. The main idea was to declare one directive and value per line, so that we can easily refer to the file without having to scroll forever horizontally. The options object only takes one value, though seeing devOnly
written next to each declaration makes it impossible to confuse.
Let's make sure our code is using it.
<meta httpEquiv='Content-Security-Policy' content={generateCSP()} />
Nothing should have changed for you, meaning it's time to fix the styling!
Inline styles
Let's have a look at the errors being thrown for our styled components:
Refused to apply inline style because it violates the following Content Security Policy directive: "default-src 'none'". Either the 'unsafe-inline' keyword, a hash ('sha256-...='), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'style-src' was not explicitly set, so 'default-src' is used as a fallback.
This message is different to the previous ones, and there's more information to digest. It's essentially telling us that the browser blocked our inline styling because our style-src
directive needs to include one of the following:
- the
'unsafe-inline'
keyword - a sha256 (or similar) hash
- a nonce value
The first option is to use the 'unsafe-inline'
keyword. As the name suggests, this method is considered unsafe as it will allow any inline styles to run, regardless of whether they were present when the site loaded or added in later by a malicious third party. It somewhat defeats the purpose of having a policy in place.
The second option is to generate a sha256 hash of our inline styles and pass the hash as one of the values for our style-src
directive. This could work in theory, but if you're using any form of CSS-in-JS solution, just one piece of conditional rendering logic (eg. a user clicks a button, changing a colour) would break this; when the CSS of the inline style changes, it wouldn't match the hash anymore.
The third option is likely our best bet, a nonce value. A nonce is a randomly generated token used only once (Number Only used oNCE). The theory is that our server will generate this random token when the user visits the site, attach the nonce as a value to our style-src
directive, and also pass the value to Next.js to attach it as a nonce=''
attribute on our style tags.
This is where this post diverges depending on your infrastructure. In order to generate a nonce value, we need our site to be behind a server. More specifically, we need every page on our site to be server-side rendered at runtime by having getInitialProps()
in a custom _app
. This likely isn't possible if you have any kind of static site, such as this one.
Let's explore some options below.
Working without a server
If you don't have a custom _app
with getInitialProps
set up, you'll need to declare 'unsafe-inline'
in your style-src
directive for the site to work with styled components (or any library that creates inline styles). I think that is okay.
So long as we're talking exclusively about using 'unsafe-inline'
in the context of styling and never for scripts, my opinion is that you should be fine. If your Content Security Policy is strong for all other directives (eg. img-src
), there is very little that CSS-based XSS attacks can achieve.
If your site does allow user input, it should go without saying that sanitising HTML to prevent custom <style>
tags or inline style
attributes before storing/rendering it is crucial in mitigating even the least dangerous forms of attack.
// style-src
add('style-src', `'unsafe-inline'`);
For further relief (at the time of writing), both Twitter's report and Spotify's report on Mozilla Observatory both include 'unsafe-inline'
for styling, but their overall scores couldn't be higher. We're always limited in some way by the architecture we choose, though it's clear that achieving a top-grade security score is doable regardless of how you're using Next.js.
Again, I am by no means a security expert so do conduct additional research if this is very important for you. Read on for information on using a nonce rather than 'unsafe-inline'
.
(Optional) Inline scripts
If your site uses any inline scripts, this section discusses how to allow those through your Content Security Policy without using the 'unsafe-inline'
keyword. If your site is completely server-side rendered (eg. a custom _app
with getInitialProps
) then you'll instead be able to use the nonce we generate later on, so feel free to skip this.
You may have noticed the same error as for inline styles. The one difference here is that we should never use 'unsafe-inline'
for script directives, as that can have massive security implications (to a point where having CSP is arguably useless!).
Instead, let's create a helper function that will hash our inline scripts.
Note that you'll only need this for inline scripts; if you have any <script>
tags with a src
attribute, you won't need to hash those - but will need to allow their origins in your CSP. If you have any scripts of type="application/json"
, they also won't need hashing as the browser doesn't evaluate those.
// hash-inline-scripts.js
import crypto from 'crypto';
const hash = content =>
crypto.createHash('sha256').update(content).digest('base64');
const hashInlineScripts = scripts => {
const scriptHashes = scripts.map(script => {
const content = script.props.dangerouslySetInnerHTML
? script.props.dangerouslySetInnerHTML.__html
: script.props.children;
return `'sha256-${hash(content)}'`;
});
return { scriptHashes, scripts };
};
export default hashInlineScripts;
// hash-inline-scripts.ts
import crypto from 'crypto';
const hash = (content: string) =>
crypto.createHash('sha256').update(content).digest('base64');
const hashInlineScripts = (scripts: JSX.Element[]) => {
const scriptHashes = scripts.map(script => {
const content = script.props.dangerouslySetInnerHTML
? script.props.dangerouslySetInnerHTML.__html
: script.props.children;
return `'sha256-${hash(content)}'`;
});
return { scriptHashes, scripts };
};
export default hashInlineScripts;
This helper function will receive an array of <script>
tags, hash their values, and return the original array. For all inline scripts we wish to embed in the site, we'll want to call this function before calling our generateCSP()
function, like so:
const { scriptHashes, hashes } = hashInlineScripts([
<script dangerouslySetInnerHTML={{ __html: `console.log('a')` }} />,
<script dangerouslySetInnerHTML={{ __html: `console.log('b')` }} />
]);
We'll want to update our generateCSP()
function to accept these hashes:
const generateCSP = ({ scriptHashes } = {}) => {
/* ... */
// script-src-elem
add('script-src-elem', `'self'`);
scriptHashes && scriptHashes.forEach(hash => add('script-src-elem', hash));
/* ... */
};
interface generateCSPProps {
scriptHashes?: string[];
}
const generateCSP = ({ scriptHashes }: generateCSPProps = {}) => {
/* ... */
// script-src-elem
add('script-src-elem', `'self'`);
scriptHashes && scriptHashes.forEach(hash => add('script-src-elem', hash));
/* ... */
};
And, lastly, update our head to pass these hashes in, as well as render the scripts:
<Head>
<meta
httpEquiv='Content-Security-Policy'
content={generateCSP({ scriptHashes })}
/>
{scripts}
</Head>
Alas, our inline scripts should now be executing!
The only caveat is that the content of your inline script can't change whilst the user is on the site, as this will invalidate the hash that was generated and will be blocked from running. I believe this also won't work with next/script tags.
Generating a nonce server-side
Whilst the following process is arguably more secure than using 'unsafe-inline'
for styling, you may not need it. The process is somewhat convoluted and may cost more time than it's worth. Nonetheless, let's go!
As mentioned above, we can only generate a nonce value on the server before passing it down to our frontend. As such, we'll need a custom _app
for this. At the time of writing, Next.js Middleware has been released but there is no information on passing data from the middleware to the custom _app
.
If you're using a custom server to wrap your Next.js frontend (such as Express), you'll still need to ensure every Next.js page is server-side rendered, so you'll still need a custom _app
with getInitialProps
.
The first step is to generate the random token. The MDN Docs recommend the nonce to be a "base64-encoded string of at least 128 bits of data from a cryptographically secure random number generator," so let's do just that by creating a util function:
// utils/generate-nonce.js
import crypto from 'crypto';
const generateNonce = () => {
return crypto.randomBytes(16).toString('base64');
};
export default generateNonce;
// utils/generate-nonce.ts
import crypto from 'crypto';
const generateNonce = (): string => {
return crypto.randomBytes(16).toString('base64');
};
export default generateNonce;
We're using the built-in crypto
library here, though feel free to use any cryptographically-secure generator (such as the uuid
library).
As mentioned earlier on, a Content Security Policy can be added to a site either as a HTTP header returned by the web server, or as a meta tag within the head. As we'll be generating the nonce on the server, we might as well have our entire middleware handle the CSP as well!
Let's remove the <meta httpEquiv='Content-Security-Policy' />
tag from our frontend โ we're going to move it to be a HTTP header instead.
For the next steps, we'll be generating the nonce in our _document
file and returning it as a header. If you have a custom server set up (eg. Express), it might be more logical for you to send the Content-Security-Policy
header from a middleware there instead. That's absolutely fine, though just be sure to pass the generated nonce to the frontend in the res.locals (or equivalent).
Let's add some lines to our _document
file. You should have one set up already if you're using styled components, though you can copy the Custom Document
template if needed.
Within the getInitialProps
function:
// pages/_document.jsx
const nonce = generateNonce();
ctx.res.setHeader('Content-Security-Policy', generateCSP({ nonce }));
// pages/_document.tsx
const nonce = generateNonce();
ctx.res.setHeader('Content-Security-Policy', generateCSP({ nonce }));
You've likely noticed that our generateCSP
function doesn't accept an argument for the nonce โ let's quickly fix that. If you followed the inline scripts section, you'll also accept an argument for scriptHashes
here.
// utils/generate-csp.js
const generateCSP = ({ nonce }) => {
/* ... */
// script-src-elem
add('script-src-elem', `'self'`);
add('script-src-elem', `'nonce-${nonce}'`);
// style-src
add('style-src', `'nonce-${nonce}'`);
add('style-src', `'unsafe-inline'`);
/* ... */
};
// utils/generate-csp.ts
interface generateCSPProps {
nonce?: string;
}
const generateCSP = ({ nonce }: generateCSPProps = {}) => {
/* ... */
// script-src-elem
add('script-src-elem', `'self'`);
add('script-src-elem', `'nonce-${nonce}'`);
// style-src
add('style-src', `'nonce-${nonce}'`);
add('style-src', `'unsafe-inline'`);
/* ... */
};
After reloading the site, something peculiar happens: our scripts still work fine, but the styling has disappeared again. We didn't remove 'unsafe-inline'
from our CSP, so what gives? The developer console in Chrome provides a great explanation:
'unsafe-inline' is ignored if either a hash or nonce value is present in the source list.
As a result of adding the nonce to our CSP, our 'unsafe-inline'
styling is no longer being rendered. Since we haven't yet linked the nonce with our inline styles, the browser doesn't trust any of them.
Certain libraries, including styled-components, allow for the nonce attribute to be provided via a __webpack_nonce__
variable, which is used primarily by webpack to inject the nonce attribute to any scripts loads. To add this to our frontend, we simply need to inject it via a <script>
tag in the document head.
Back inside our custom _document
file:
Just after the initialProps
variable declaration, add:
const initialProps = await Document.getInitialProps(ctx);
const additionalProps = { nonce }; // ๐ add this
return {
...initialProps,
...additionalProps, // ๐ and this!
styles: /* ... if using styled-components ... */
};
Within the render()
method, before the return
statement, add:
const { nonce } = this.props;
And within the <Head>
tag, add:
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.__webpack_nonce__ = "${nonce}"`
}}
/>
Before the MyDocument
class declaration, add:
interface DocumentProps {
nonce: string;
}
Update the MyDocument
class to extend this:
export default class MyDocument extends Document<DocumentProps>
Just after the initialProps
variable declaration, add:
const initialProps = await Document.getInitialProps(ctx);
const additionalProps = { nonce }; // ๐ add this
return {
...initialProps,
...additionalProps, // ๐ and this!
styles: /* ... if using styled-components ... */
};
Within the render()
method, before the return
statement, add:
const { nonce } = this.props;
And within the <Head>
tag, add:
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.__webpack_nonce__ = "${nonce}"`
}}
/>
The site should now have some unique behaviour: the styling appears, but also seems to take a second to appear. If we disable JavaScript in our browser, the styling will be gone completely.
This is happening because our __webpack_nonce__
variable only exists on the window
object, which doesn't exist on the server. As such, styled-components only pulls the value after the app rehydrates on the client side.
Unfortunately, there is no clean way of passing our nonce value directly through to styled-components yet. The previous method I suggested in this post last year seems to no longer work, though thankfully I've discovered a less-hacky (but still hacky) solution!
Just after the additionalProps
variable declaration, add:
const sheetStyles = sheet.getStyleElement();
const style =
(sheetStyles &&
React.Children.map(sheetStyles, child =>
React.cloneElement(child, {
nonce
})
)) ||
null;
Finally, pass the style
variable into your return
statement:
return {
...initialProps,
...additionalProps,
styles: (
<>
{initialProps.styles}
{style}
</>
)
};
Just after the additionalProps
variable declaration, add:
const sheetStyles = sheet.getStyleElement();
const style =
(sheetStyles &&
React.Children.map(sheetStyles, child =>
React.cloneElement(child, {
nonce
} as React.StyleHTMLAttributes<HTMLStyleElement>)
)) ||
null;
Finally, pass the style
variable into your return
statement:
return {
...initialProps,
...additionalProps,
styles: (
<>
{initialProps.styles}
{style}
</>
)
};
The site should now be fully styled, even with JavaScript disabled and before the client-side rehydration! Essentially, we're cloning the React <style>
elements that styled-components is generating for us, but passing in our nonce value as an attribute.
And that's all there is to it! I hope this post has been insightful enough to help explain not only how to set strong Content Security Policy headers, but also what their purpose is for in general.
Go forth and make strong websites!
Bonus: Google Fonts
If you're using the 'follow-along' repository, or just have Google Fonts embedded into your website generally, you may notice the fonts are still not loading.
This is again due to the fact that we haven't whitelisted the domain that the fonts come from. Thankfully with CSP headers, we don't need to whitelist a specific URI, but can whitelist an entire domain (or subdirectory!) instead.
Taking a look at the console, the styling is missing because the Google Fonts domain is missing in our style-src
directive. Let's add it to our generateCSP
function:
add('style-src', 'https://fonts.googleapis.com');
After reloading, you'll notice another error related to a missing font-src
directive value. Let's add that as well:
add('font-src', 'https://fonts.gstatic.com');
Notice that this time we didn't include the 'self'
value? If you aren't hosting any fonts within the project itself (meaning no fonts will be loaded from the same frontend URL), there's really no need to add it.
And after reloading one final time, the fonts should appear!
Bonus 2: Maximising our CSP
The Mozilla Observatory recommends adding a few extra headers that we haven't yet specified in order to maximise the strength of our CSP.
The frame-ancestors directive can be used to prevent clickjacking, by limiting which origins will be able to embed ours in an <iframe>
or similar.
add('frame-ancestors', `'none'`);
The base-uri directive limits which origins will be able to reference ours in a <base>
element, which could be used to trick our site into loading untrusted scripts.
add('base-uri', `'none'`);
The form-action directive restricts which origins <form>
elements are allowed to submit to, which can prevent sensitive data from being submitted to a malicious domain. This only affects the action
attribute on forms (and not JavaScript that might call preventDefault()), so you'll only need to include 'self'
(or another origin) here if your forms need to be able to submit without JavaScript.
add('form-action', `'none'`);
Bonus 3: Maximum security
I mentioned it slightly earlier, but Next.js has a great documentation page on Security Headers that can really increase a website's security rating (especially on the SecurityHeaders website).
For the most part, the list of options on the page can simply be copy/pasted into your next.config.js
file. It's a small addition that can add some great security to your site.
It's also potentially possible to call your generateCSP
function in this file instead, though it won't work if you need a nonce value, and you might need to re-code the function to be CommonJS-friendly since you can't use ES Modules there just yet.
And, just like that, we've got an amazingly strong Content Security Policy setup. Neat!