In this post, we’re going to cover content security policy, or CSP, in React. First, we’ll have a brief overview of CSP: what is it good for anyway? Then we’ll show you a few ways to enable CSP in React. React has a rich ecosystem that includes several frameworks and hosting platforms, and we can only cover a few of them here, but you’ll get the idea. Last, there are a few caveats to be aware of, including some specific to React. Pay special attention to these.
Let’s start with some basics.
What Is CSP?
Content Security Policy (CSP) is a critical web security standard that helps prevent Cross-Site Scripting (XSS) attacks, clickjacking, and other code injection attacks. CSP works by allowing developers to control which resources the browser is permitted to load for a specific page, effectively creating an allowlist of trusted content sources.
When properly implemented, CSP acts as a defense-in-depth security layer, significantly reducing the risk of XSS attacks even when other security measures fail. The policy is defined in HTTP headers (or meta tags) and is enforced by all major modern web browsers. If malicious scripts or unauthorized content attempt to execute on your page, CSP blocks them and can optionally report the violation.
Let’s examine an example of CSP in action to clarify how this protection works. The simple React page below has a Content Security Policy that implicitly disallows content from wikimedia.com.
Content Security Policy: The page's settings blocked the loading of a resource
at https://upload.wikimedia.org/example.jpg because it violates the following
directive: "img-src 'self'"
As you can see, the image is not rendered. The content security policy blocks it. The policy for this page only allows content from the host, the source, and one other domain (for loading React development scripts). The image itself is blocked by the browser. You can see this in the network tab of DevTools in Chrome.
How to Enable Content Security Policy in React
You can enable a CSP in two different ways in a React app. The first is to add the headers directly to the response. The second is to add meta tags to the content. Note that meta tags aren’t supported for some security headers, such as HSTS.
Always prefer setting the CSP via HTTP response headers. Meta tag-delivered CSPs only work for a subset of directives and are not as secure or comprehensive as those delivered via HTTP headers. Use meta tags only in contexts where headers cannot be set (e.g., static file hosts without access to header configuration).
Let’s explore them, starting with a basic React app and ending with options for applying a CSP policy on the server.
ReactJS
If you need to control a CSP using React code only, you’re going to be using meta tags. Meta tags are placed in the head section of the HTML document. Here’s a basic example of how to accomplish this on the index page:
<html>
<head>
...
<meta http-equiv="Content-Security-Policy"
content="...policy definition here..." />
</head>
<body>
...
<div id="app-root"></div>
...
</body>
</html>
The meta tag contains the Content Security Policy (CSP) directive. Browsers read and interpret meta tags in the head before processing the body. Still, you may also have content, such as scripts and CSS, that is loaded in the head.
Order is important! To apply the CSP to all scripts loaded initially, place this meta tag before script or style tags. Meta-based CSPs must appear in the <head>
before any script/style tag โ otherwise, they’re ignored or too late to protect anything. Alternatively, you could place the meta tag after scripts and style tags to avoid adding complexity to the CSP. Of course, doing so will diminish the effectiveness of the security mechanism.
There is an npm package named react-csp that helps apply CSP to React apps, but it is of limited value compared to other options. The react-csp package simply adds syntactic sugar to how you add the CSP meta tag to the head. It does all the formatting too. There is certainly value in using it to create a meta tag as you become familiar with the syntax. To continue using it, however, you have to add another CLI command to your build script.
If you create a CSP in a pure React app, you’ll need to keep track of everything. It would be much better to have more granular control. Next.js or some other SSR can give you that.
Vite
For React applications using Vite, you can implement CSP through plugins and configuration.
Note that html.cspNonce
is not available in Vite core and only works if used with compatible HTML plugins (like @vitejs/plugin-html
). You’ll need custom logic to inject CSP-compliant nonces into your templates.
Here’s how to set up CSP with Vite using a plugin approach:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
assetsInlineLimit: 0 // Disable inlining for stricter CSP
}
})
When using Vite, you’ll typically implement CSP through your hosting provider or server configuration rather than through Vite itself.
For development, you may need to allow additional sources. Note that 'unsafe-eval'
should never appear in production policies as it entirely defeats the purpose of CSP:
// For development server
export default defineConfig({
server: {
headers: {
'Content-Security-Policy': `
default-src 'self';
script-src 'self' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
connect-src 'self' ws: wss:;
`.replace(/\s{2,}/g, ' ').trim()
}
}
})
Let’s take a look at how to add CSP in Next.js.
Next.js
Next.js gives you more options for adding a CSP to your React app. Since Next.js renders the React app on the server prior to delivering it to the client, it can be more dynamic. Remember, delivering the CSP via meta tags only works when the meta tags are in the head of the document. If you put them in the body, you’ll get an error that looks a bit like this:
The Content Security Policy 'img-src upload.wikimedia.org' was
delivered via a <meta> element outside the document's <head>,
which is disallowed. The policy has been ignored.
So let’s take a look at some options that will allow you to set your policy headers more dynamically.
Add CSP Headers in Your next.config.js
Until now, we’ve been examining the ability to enable CSP via meta tags. Now you’ll see how to add the policy in response headers. To tackle this in Next.js, set the headers in the config file. The next.config.js file is a normal JavaScript file that controls several aspects of how Next.js behaves. One such option is to add response headers per page. Here’s an example of what this configuration might look like:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim()
}
]
}
]
}
}
module.exports = nextConfig
This configuration will apply the policy to all routes. Unlike the original example that only applied to the root, this modern approach covers your entire application.
Add Meta Tags Using next/head
You could add a CSP in meta tags using the Next.js built-in Head component. It’s commonly used to update the page title and other page metadata. To add a CSP using this component, simply import it from next/head and use it as follows:
import Head from 'next/head'
...
return (
<div>
<Head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' https: ; object-src 'none'" />
</Head>
</div>
)
...
This snippet will append a CSP to the head of the page. It will not overwrite or override existing policies. Itโs important to note that CSP headers do not stack; this means the browser uses only the first valid policy it encounters. Avoid sending multiple CSPs. This meta tag approach should only be used if you’re unable to send headers, as header-based CSPs take precedence.
Add a CSP Using Middleware
Next.js middleware provides a powerful way to implement CSP. This approach allows you to generate unique nonces for each request:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
export function middleware(request: NextRequest) {
ย ย // Generate a unique nonce for each request
ย ย const nonce = crypto.randomBytes(16).toString('base64')
ย ย const cspHeader = `
ย ย ย ย default-src 'self';
ย ย ย ย script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
ย ย ย ย style-src 'self' 'nonce-${nonce}';
ย ย ย ย img-src 'self' data: blob: https:;
ย ย ย ย font-src 'self';
ย ย ย ย object-src 'none';
ย ย ย ย base-uri 'self';
ย ย ย ย form-action 'self';
ย ย ย ย frame-ancestors 'none';
ย ย ย ย upgrade-insecure-requests;
ย ย `
ย ย // Replace newline characters and spaces
ย ย const contentSecurityPolicyHeaderValue = cspHeader
ย ย ย ย .replace(/\s{2,}/g, ' ')
ย ย ย ย .trim()
ย ย const requestHeaders = new Headers(request.headers)
ย ย requestHeaders.set('x-nonce', nonce)
ย ย requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue)
ย ย const response = NextResponse.next({
ย ย ย ย request: {
ย ย ย ย ย ย headers: requestHeaders,
ย ย ย ย },
ย ย })
ย ย response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue)
ย ย return response
}
export const config = {
ย ย matcher: [
ย ย ย ย {
ย ย ย ย ย ย source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
ย ย ย ย ย ย missing: [
ย ย ย ย ย ย ย ย { type: 'header', key: 'next-router-prefetch' },
ย ย ย ย ย ย ย ย { type: 'header', key: 'purpose', value: 'prefetch' },
ย ย ย ย ย ย ],
ย ย ย ย },
ย ย ],
}
Note that the Next.js middleware runs only on Edge Runtime and cannot modify the response body. You must ensure your rendering code reads the x-nonce
value and includes it in <script nonce={nonce}>
tags, as shown in the example below.
// app/page.tsx
import { headers } from 'next/headers'
export default function Page() {
ย ย const nonce = headers().get('x-nonce')
ย ย return (
ย ย ย ย <div>
ย ย ย ย ย ย <h1>My App</h1>
ย ย ย ย ย ย <script
ย ย ย ย ย ย ย ย nonce={nonce}
ย ย ย ย ย ย ย ย dangerouslySetInnerHTML={{
ย ย ย ย ย ย ย ย ย ย __html: `console.log('This script is allowed by CSP')`
ย ย ย ย ย ย ย ย }}
ย ย ย ย ย ย />
ย ย ย ย </div>
ย ย )
}
Add a CSP Using @next-safe/middleware
There’s also a community package called @next-safe/middleware
that provides strict CSP support for Next.js applications. You can install it in your app using:
npm install @next-safe/middleware
In your app itself, it can be used like this:
// middleware.ts
import { NextRequest } from 'next/server'
import { chainMiddleware, csp, strictDynamic } from '@next-safe/middleware'
const securityMiddleware = [
ย ย csp({
ย ย ย ย directives: {
ย ย ย ย ย ย 'default-src': "'self'",
ย ย ย ย ย ย 'object-src': "'none'",
ย ย ย ย ย ย 'base-uri': "'self'",
ย ย ย ย ย ย 'form-action': "'self'",
ย ย ย ย ย ย 'frame-ancestors': "'none'",
ย ย ย ย },
ย ย }),
ย ย strictDynamic(),
]
export default chainMiddleware(...securityMiddleware)
Web Server
You can also set your CSP headers via the web server. This won’t always be an option, as you may not always have access or the ability to change server configurations, and it may not even be the best option if you do, due to coupling issues and maintenance overhead between environments. However, it’s possible. For example, an NGINX server can set response headers at the HTTP, server, and location levels. This provides you with some flexibility to establish distinct policies for various contexts. It does not, however, make it easy to alter the policy along with the code.
Let’s say, for example, that you added a new script source from a third-party provider. In that case, you would also need to update your CSP to allow the script to load. To do that, you’d have to update the NGINX configuration, deploy it, and restart the NGINX process. Going through those steps might not be the best option for you. Still, it’s good to know the option exists.
Here’s an example NGINX configuration:
location / {
ย ย ย ย add_header Content-Security-Policy "
ย ย ย ย ย ย default-src 'self';
ย ย ย ย ย ย script-src 'self';
ย ย ย ย ย ย style-src 'self' 'unsafe-inline';
ย ย ย ย ย ย img-src 'self' data: https:;
ย ย ย ย ย ย font-src 'self';
ย ย ย ย ย ย object-src 'none';
ย ย ย ย ย ย base-uri 'self';
ย ย ย ย ย ย form-action 'self';
ย ย ย ย ย ย frame-ancestors 'none';
ย ย ย ย " always;
ย ย ย ย try_files $uri $uri/ /index.html;
}
React CSP Gotchas
One of the biggest challenges when implementing CSP in React applications is handling inline scripts. By default, CSP blocks all inline JavaScript for security reasons, but React apps often generate inline scripts during the build process or at runtime.
When you enable a strict CSP, you might see errors like:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'"
You have several options to resolve this, each with different security trade-offs. Here they are listed from least to most secure:
- Allow all inline scripts – Add
'unsafe-inline'
to yourscript-src
directive (least secure) - Use nonces – Generate unique tokens for each inline script (more secure, but complex)
- Use hashes – Create SHA hashes for specific inline scripts (secure, but requires build-time generation)
- Eliminate inline scripts entirely – Configure your build to avoid inline scripts altogether (most secure)
From a practical standpoint, avoiding inline scripts entirely is the most secure approach. However, during early development, some teams temporarily allow ‘unsafe-inline’ before transitioning to nonces or hashes.
Additional CSP Challenges
CSS-in-JS Libraries: Many React applications utilize CSS-in-JS libraries, such as styled-components, Emotion, or Material-UI. These libraries often inject inline styles, which CSP blocks by default. Here’s how to handle them:
// For styled-components with nonce
import { StyleSheetManager } from 'styled-components'
function App() {
ย ย const nonce = useNonce() // Get nonce from your CSP setup
ย ย return (
ย ย ย ย <StyleSheetManager nonce={nonce}>
ย ย ย ย ย ย <MyStyledComponents />
ย ย ย ย </StyleSheetManager>
ย ย )
}
Development vs Production: Development servers often require more permissive policies due to hot reloading and dev tools. It is critical to never use 'unsafe-eval'
in production:
// Development CSP (permissive for tooling)
const devCSP = `
ย ย default-src 'self';
ย ย script-src 'self' 'unsafe-eval' 'unsafe-inline';
ย ย style-src 'self' 'unsafe-inline';
ย ย connect-src 'self' ws: wss:;
`
// Production CSP (strict and secure)
const prodCSP = `
ย ย default-src 'self';
ย ย script-src 'self' 'nonce-\${nonce}' 'strict-dynamic';
ย ย style-src 'self' 'nonce-\${nonce}';
ย ย connect-src 'self';
`
Remember, strict-dynamic
only works when used with nonces or hashes on a trusted bootstrap script. It ignores 'self'
and 'unsafe-inline'
when used.
React 19 Considerations
If you’re using React 19, note that it introduces new features that may affect CSP. These features include:
- Server Actions may require specific
form-action
policies - Enhanced hydration might need careful script-src configuration
- New built-in components could affect CSP requirements
Let’s walk through a couple of possibilities of how a CSP might be circumvented.
- A script that loads before the meta tag might insert unwanted content before a CSP meta tag.
- A third party between the user agent and the server may be able to add or remove tags or headers before delivering the response.
- Overly permissive policies may leave the page vulnerable to nefarious content.
Additionally, Google Research published a document in 2016 outlining concerns with CSP. Their research indicated that over 99 percent of web pages that used a CSP were still vulnerable to cross-site scripting (XSS) by other means of circumvention. Recent studies continue to show that many CSP implementations are ineffective due to overly permissive policies.
Validating Your React CSP Configuration with StackHawk
Instead of waiting for your security team to discover these issues later in the development lifecycle or when they hit production, let’s use StackHawk to automatically identify potential vulnerabilities for us. As a modern DAST platform, StackHawk scans your running application and tests it for issues, such as CSP misconfiguration, XSS, and other critical vulnerabilities in the OWASP Top 10.
To do this, you’ll need to ensure you have a StackHawk account. If you need one, you can sign up for a trial account or log into an existing account.
If you’re logging into an existing StackHawk account, from the Applications screen, you’ll click Add Application.
If you’re new to StackHawk, you’ll be automatically brought into the Add an App flow. On the Scanner screen, you’ll see the instructions for installing the StackHawk CLI. Since we will be running our testing locally, we will use this option. Once the hawk init command is executed successfully, click the Next button.
On the next screen, you will fill out an Application Name, Environment, and URL. Once filled out, click Next.
Since we will be testing a React application with web pages, on the next page, we will choose our Application Type as “Dynamic Web Application/Single Page Application”.
Depending on your React app setup, you’ll also configure the application details. For example, if you have API endpoints in your React app, you would set the API Type to “REST / OpenAPI” and point to your OpenAPI specification file by selecting URL Path and adding the path to where your OpenAPI spec is located. Alternatively, you can click “Skip for now” if you’re unsure. Once complete, click Create App.
Lastly, we will need to add a stackhawk.yml file to the root of our React project. Once the file is added, copy the screen’s contents, paste them into the file, and save it. Lastly, we can click the Finish button.
In our root directory, you should see the stackhawk.yml file we’ve added:
Run HawkScan
Now weโre ready to test our React application. In a terminal pointing to the root of our project, we will run HawkScan using the following command:
hawk scan
After running the command, the tests will execute in the terminal.
Note: If you get an error similar to the one below, this means that your React application is not running in HTTPS, and that is how HawkScan is trying to call the app:
HawkScan Target Not Found Error: Unable to access https://localhost:3000. Check if the web server is listening on the specified port.
To fix this, either add HTTPS capabilities to your React app or, more simply, change the host entry in your stackhawk.yml to use “http”.
This will run tests against your React application and its pages. Once the tests have run, we can begin to explore any findings that were discovered.
Explore The Initial Findings
Once the tests are complete, the terminal will contain some information about any vulnerabilitiesโincluding CSP vulnerabiliesโfound. In the below screenshot, you can see that StackHawk has identified CSP vulnerabilities across multiple pages within the React application.
To explore the vulnerabilities further, we will click on the test link at the bottom of the output. This will take us into the StackHawk platform to explore further.
After clicking on the link, we can now view the test results in a nicely formatted display. This helps us confirm that the CSP issue found was, in fact, fixed by our updated implementation. StackHawk helps us to confirm this after the rescan. Next, we will click on the CSP Misconfiguration entry.
Within this entry, we can see an Overview and Resources that can help us with fixing this vulnerability, as well as the Request and Response that the application returned on the right side of the screen. Above this, you will also see a Validate button, which will display a cURL command with the exact HTTP request used to expose the vulnerability.
Understand and Fix the React CSP Security Issue
When StackHawk identifies CSP vulnerabilities, you’ll see detailed findings that indicate which paths are affected and provide potential remediation techniques (as shown in the image above). Using the techniques in this guide or the remediation advice provided by StackHawk for the vulnerability, you can then make the necessary adjustments to your code and configuration. Once fixed, ensure that you stop your web servers and redeploy the latest code.
Confirm the Fix!
With the latest configuration deployed, we can use StackHawk to confirm that the fix implemented actually resolves our CSP misconfiguration issues and is secure. To do this, we will click the Rescan Findings button back in StackHawk.
Then, we will see a modal containing the “hawk rescan” command that includes the correct Scan ID. You’ll run this command in the same terminal where you ran the initial set of tests.
In the output, you will again see any vulnerabilities that were found during the scan. In this case, you’ll see that the CSP misconfiguration vulnerabilities are no longer showing. Clicking on the link at the bottom of the terminal output, you can confirm that the CSP misconfiguration vulnerabilities have now been added to the Fixed Findings from Previous Scan, confirming that the vulnerability has been successfully fixed and has passed any vulnerability tests.
With that, we’ve successfully remedied and retested our React application to ensure its safety from potential CSP misconfiguration attacks. With StackHawk, this can be done on a local machine (as demonstrated here) or automatically as part of a CI/CD flow. Regardless of how itโs used, StackHawk provides developers with direct insight into the vulnerabilities present within their code and the tools to fix and validate those fixes.
Final Thoughts on React CSP
A CSP is one of several security controls for the web. As you’ve seen, it’s somewhat complex to manage, and it can be thwarted. Itโs important to implement a CSP, but equally important to take the time to ensure you have a thorough understanding of the policy as a whole. You’ll inevitably encounter some edge cases, but hopefully you’ve learned enough here to deal with them as they arise. If youโd like to see how CSP works in other JavaScript frameworks, check out our other guides:
- Angular CSP – How to implement CSP in Angular apps
- Vue CSP – Best practices for Vue CSP configuration
The React ecosystem has evolved significantly since CSP was first introduced, with frameworks like Vite providing built-in CSP support and Next.js offering powerful middleware capabilities. While the fundamentals remain the same, modern tools make it easier to implement secure, maintainable CSP policies.
Remember that security is a continuous process, and the best way to keep up as your application grows is to automatically test for CSP vulnerabilities in pre-production. Once you’ve implemented CSP headers in your React application, it’s crucial to ensure your configuration is both functional and secure. Instead of manually testing every possible CSP scenario or every potential vulnerability, you can use StackHawk to automatically validate that your application’s security posture and CSP implementation work correctly without creating new attack vectors.