Using Content Security Policy headers with React & emotion
Tags: frontend, security, react
Published: 2023-09-24 12:00:07

Content Security Policy (CSP) headers add another layer of security by disallowing unsafe actions, such as establishing connections with arbitrary domains, usage of `eval`, inline scripts, and others. This article will focus on the `style-src` directive and its usage with `emotion`.

## Using CSP headers

`Content-Security-Policy` header should be set in the response to the browser when the application page is requested (e.g. `index.html`). It looks like this:

```
Content-Security-Policy: style-src self;
```

`style-src` is a _directive_ specifying which styles are allowed on the page. Possible values include:
- `self` - styles served from the same domain
- URL, e.g. `https://example.test`
- `unsafe-eval` - dynamically creating stylesheets from strings, e.g. by `CSSStyleSheet.insertRule()`
- `unsafe-inline` - inline `<style></style>` tags
- `nonce-<value>`, e.g. `nonce-abc` - allows creating inline style tags _only_ with the specified `nonce` attribute value. The value should be coming from a cryptographically secure random token generator and re-created on every request.

Modifying element styles via DOM APIs that do not involve CSS parsing is allowed unless JS execution is blocked by the `script-src` directive. The following would work:
```
element.style.display = "none";
```

The following will not work unless `unsafe-eval` is enabled:
```
element.setAttribute("style", "display: none;");
```

## CSP headers and emotion

`emotion` inserts inline `style` tags and cannot be currently configured to extract styles to a static file at build-time. It means that if we need to enable CSP headers with `emotion` the following options are available:
1. `unsafe-inline`. This option doesn't require any frontend code changes but it will not bring any security benefits.
2. `nonce-<value>`. This option can be used to only allow inline style tags created by `emotion` in the app. It is the most secure approach with `emotion`. To make it work, `emotion` needs to be aware of the `nonce` value set in the page response headers. The exact configuration depends on how `emotion` is used in your app.

If you are using `@emotion/react` or `@emotion/styled` you would need to provide a custom cache with `nonce` set:

```
import { CacheProvider } from '@emotion/react'
import createCache from '@emotion/cache'

export function App() {
  const cache = createCache({
    key: 'my-app',
    nonce: getNonceValue(),
  });

  return (
    <CacheProvider cache={cache}>
      ...
    </CacheProvider>
  );
}
```

If you are using `@emotion/css` you would need to create a custom `emotion` instance:

```
import createEmotion from '@emotion/css/create-instance'

export const {
  flush,
  hydrate,
  cx,
  merge,
  getRegisteredStyles,
  injectGlobal,
  keyframes,
  css,
  sheet,
  cache
} = createEmotion({
  key: 'my-app',
  nonce: getNonceValue(),
});
```

Please note that with this approach you would need to change all places where you previously used `@emotion/css` directly to import the module where `createEmotion` is called:

```
// import { css } from "@emotion/css"; 
import { css } from "./emotion.ts";
```

## Passing nonce value to the frontend

CSP headers cannot be inspected by the frontend, so the value should be also set elsewhere. One of the common approaches is to set the value in an inline `script` tag by the backend:

```
<script id="nonce" type="application/json">
  "abc"
</script>
```

The frontend could use it later like this:
```
function getNonceValue() {
  const nonceElement = document.getElementById("nonce");
  return JSON.parse(nonceElement.textContent);
}
```

Please note that there is a custom type attribute on the `script` tag. This is required to make the browser ignore the script body and not to execute it, which won't work with certain `script-src` directive values.