Secure StaticRouter in React

Securing React applications that use server-side rendering (SSR) requires a different approach than securing standard client-side applications. This article explains how to secure the StaticRouter in React by validating user authentication on the server, managing unauthorized access redirects using the router’s context object, and protecting against common hydration vulnerabilities.

Authenticate on the Server First

Because StaticRouter is designed for stateless, server-side rendering, client-side route guards (like checking localStorage in a useEffect hook) will not work during the initial render. Security must begin on the web server (e.g., Express, Koa, or Next.js) before React ever attempts to render the application.

  1. Verify Session Cookies or JWTs: Read the incoming request headers to validate the user’s authentication token.
  2. Reject Invalid Requests Early: If a request is made to a highly sensitive API route or page that requires strict server-side rendering, block the request at the middleware level before calling renderToString.

Manage Redirects Using the Context Object

When using StaticRouter, React renders the entire component tree to a string on the server. If a user attempts to access a protected component within that tree, you need a way to signal to the server that rendering should stop, or that the server should issue a redirect.

You can achieve this by passing a context object to the StaticRouter.

1. Create a Protected Route Component

Create a component that checks for authentication and modifies the StaticRouter context if the user is unauthorized.

import React from 'react';
import { Route, Redirect } from 'react-router-dom';

const ProtectedRoute = ({ component: Component, isAuthenticated, ...rest }) => {
  return (
    <Route
      {...rest}
      render={(props) => {
        if (isAuthenticated) {
          return <Component {...props} />;
        }

        // On the server, StaticRouter uses the context object to store redirect info
        return (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: props.location }
            }}
          />
        );
      }}
    />
  );
};

2. Handle the Context on the Server

When <Redirect> renders inside a StaticRouter, it automatically populates the context object with a url property. You must check this property on the server to perform a real HTTP redirect.

import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server'; // Or 'react-router-dom' depending on version
import App from './App';

const app = express();

app.get('*', (req, res) => {
  const context = {};
  const isAuthenticated = checkUserAuth(req); // Your server-side auth logic

  const html = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App isAuthenticated={isAuthenticated} />
    </StaticRouter>
  );

  // If a Redirect component was rendered, context.url will contain the target URL
  if (context.url) {
    res.writeHead(302, { Location: context.url });
    res.end();
  } else {
    res.status(200).send(`<!DOCTYPE html><html><body>${html}</body></html>`);
  }
});

Secure Server-Side Hydration State

Often, the server will pass the authenticated user’s state to the client so the client-side React app can “hydrate” without losing the authentication status. This is usually done by injecting a global variable into the HTML template:

<script>
  window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)};
</script>

To prevent Cross-Site Scripting (XSS) attacks through this injected state:

  1. Sanitize the State: Never directly stringify user-inputted data into a <script> tag. Use libraries like serialize-javascript to sanitize the JSON string.
  2. Implement CSP: Use Content Security Policy (CSP) headers to restrict script execution and prevent malicious inline scripts from running.