React Content Security Policy (CSP) with a Nonce
How to add a CSP nonce to a React + Webpack app served by Node or Nginx
Why use a nonce?
A Content Security Policy can block inline <script> and <style> tags outright, a strong defense against injected malicious code. The common workaround, the unsafe-inline directive, defeats that protection by letting any inline script run, whether you wrote it or an attacker did.
A nonce is the safer alternative. It's a random, single-use token that the server issues with every response and attaches both to the CSP header and to each legitimate inline script or style tag. The rule is simple:
A nonce must be regenerated on every request, and it must be unguessable.
When a tag's nonce attribute matches the one in the response's Content-Security-Policy header, the browser knows that inline content was deliberately placed there by the server — not injected by a third party, so it's safe to run.
For more background, see the Content Security Policy guide on web.dev, particularly the "If you absolutely must use it" section, which shows the canonical nonce pattern.
How this fits a React + Webpack build
Webpack doesn't know the nonce at build time, it can't, since a fresh one has to be generated for every incoming request. So the build only inserts a placeholder, and the server swaps that placeholder for a real nonce as each request is handled.
1. Inject a placeholder at build time
A small custom Webpack plugin, NonceInjector, walks the HTML tags that html-webpack-plugin generates and adds a nonce="_NONCE_" attribute to each <script> and <style> tag:
<script
defer="defer"
src="/static/js/main.0405c3f1.js"
nonce="_NONCE_"
></script>
<link href="/static/css/main.f855e6bc.css" rel="stylesheet" nonce="_NONCE_" />Nonce Injector Webpack Plugin
const HtmlWebpackPlugin = require("html-webpack-plugin");
class NonceInjector {
constructor(NONCE_PLACEHOLDER) {
this.NONCE_PLACEHOLDER = NONCE_PLACEHOLDER;
}
apply(compiler) {
compiler.hooks.thisCompilation.tap("NonceInjector", (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).afterTemplateExecution.tapAsync(
"NonceInjector",
(compilation, callback) => {
const { headTags } = compilation;
headTags.forEach((tag) => {
tag.attributes.nonce = this.NONCE_PLACEHOLDER;
});
callback(null, compilation);
}
);
});
}
}
module.exports = NonceInjector;2. Replace the placeholder at request time
This is where the server comes in: every incoming request has to generate a fresh random nonce, then substitute it for _NONCE_ in both the HTML it serves and the Content-Security-Policy header it returns.
The flow looks like this:
To confirm the nonce actually changes on every request, you need a running server-and-client setup like the sample projects below use Docker for this. There are two implementations here: one with Node.js & one with Nginx.
Node.js (Express)
Express.js serves the built React app and replaces the nonce placeholder with a freshly generated nonce on every request.
GitHub: Express.js server sample project
Node uses the Buffer and Crypto modules to generate the random nonce, see server.js.
Nginx
Nginx serves the built React app and replaces the nonce placeholder with a freshly generated nonce on every request.
GitHub: Nginx server sample project
Nginx uses njs scripting to generate the random nonce string. See the Nginx config file for how it's wired in.
Verifying it works
Check the CSP header. Inspect any request and confirm the Content-Security-Policy header is present in the response, with a nonce value. Every script and style tag in that response should carry the same nonce.

Check the tags themselves. Inspect the DOM and confirm the nonce attribute is present on the script and style tags, matching the header.

Wrap-up
If you already have a React + Webpack app served by Node or Nginx, you can drop in the NonceInjector plugin above along with one of these two server setups to add CSP nonce support.
Cheers ✌️