How to Secure SSR in React
Server-Side Rendering (SSR) in React enhances performance and search engine optimization, but it also introduces unique security risks that do not exist in client-side-only applications. This article provides a straightforward guide on how to secure your React SSR applications against critical vulnerabilities, such as Cross-Site Scripting (XSS), data leaks, and server-side Denial of Service (DoS) attacks.
1. Prevent XSS during Data Hydration
The most common security vulnerability in React SSR involves
transferring state from the server to the client, a process known as
hydration. Often, developers serialize state on the server and inject it
into the HTML document inside a <script> tag like
this:
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(state)};
</script>If the state contains user-generated input with malicious code (e.g.,
</script><script>alert('XSS')</script>),
the browser will execute it.
How to fix it:
Never use raw JSON.stringify to inject data into script
tags. Instead, use serialization libraries designed to escape HTML
entities and prevent script injection.
- Use
serialize-javascript: This library sanitizes the JSON string to ensure it cannot be interpreted as executable code. - Use Redux or Next.js built-in wrappers: Modern SSR frameworks like Next.js handle this serialization securely by default.
import serialize from 'serialize-javascript';
// Safe serialization
const safeState = serialize(state, { isJSON: true });2. Sanitize Rendered HTML
React’s dangerouslySetInnerHTML is inherently risky, but
it is doubly dangerous in an SSR environment. If you render raw HTML on
the server without sanitizing it first, malicious scripts can execute on
both the server (if using certain Node environments) and the client.
How to fix it:
- Avoid using
dangerouslySetInnerHTMLwhenever possible. - If you must render rich text or raw HTML, sanitize the input before
rendering using an isomorphic sanitization library like
isomorphic-dompurify.
import DOMPurify from 'isomorphic-dompurify';
const cleanHtml = DOMPurify.sanitize(userContent);
const Component = () => <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;3. Implement a Robust Content Security Policy (CSP)
A Content Security Policy (CSP) is a critical layer of defense that restricts the resources (such as JavaScript, CSS, and images) that the browser is allowed to load.
How to fix it:
For SSR applications, use a nonce-based CSP. 1.
Generate a unique, cryptographically strong random token (nonce) on the
server for every single request. 2. Include this nonce in your CSP HTTP
response header. 3. Apply the same nonce to all inline
<script> tags in your React HTML template.
Content-Security-Policy: default-src 'self'; script-src 'nonce-RndmKey123' 'strict-dynamic';
Only scripts with the matching nonce="RndmKey123" will
execute, blocking unauthorized inline scripts injected by attackers.
4. Prevent Environment Variable Leaks
In SSR, code is executed on the server before the final page is sent to the client. It is easy to accidentally bundle sensitive server-side environment variables (like API keys, database credentials, or private tokens) into the client-side JavaScript bundle.
How to fix it:
- Enforce strict naming conventions: Frameworks like
Next.js require you to prefix client-accessible variables with
NEXT_PUBLIC_. Any variable without this prefix remains strictly on the server. - Explicitly separate configurations: Maintain a clear boundary between server-only configuration files and client-side configuration files.
5. Mitigate Server-Side Denial of Service (DoS)
React SSR is a CPU-intensive process because the server must render the entire component tree into an HTML string for every request. Attackers can exploit this by sending a high volume of requests, overloading the server’s CPU and crashing the application.
How to fix it:
- Implement Rate Limiting: Limit the number of requests a single IP address can make within a specific timeframe.
- Utilize Caching: Use a Reverse Proxy (like Nginx) or a Content Delivery Network (CDN) to cache fully rendered HTML pages for non-dynamic routes.
- Use Streaming SSR: Utilize React’s
renderToPipeableStreamto stream HTML to the client in chunks. This reduces server memory usage and improves response times under heavy load.