पेनलिजेंट हेडर

Frontend Framework Security, React and Next.js Changed the Attack Surface

Frontend frameworks used to be treated as the browser layer. Security teams looked for DOM XSS, token storage mistakes, unsafe third-party scripts, broken CORS, and API authorization gaps. Those risks still matter. What changed is that modern frontend frameworks no longer stop at the browser. React Server Components, Next.js App Router, Server Actions, Route Handlers, Proxy, streaming, prerendering, and framework-level caching moved significant parts of application execution into a layer that many teams still call “frontend.”

That label is now misleading. A Next.js application can read from a database inside a Server Component, mutate state through a Server Action, gate traffic through Proxy, stream a React Server Component payload, cache route output, and hydrate client islands in the browser. The same route can involve server-only code, serialized component state, client-side JavaScript, edge routing logic, CDN behavior, npm dependencies, and runtime configuration. A bug in any one of those boundaries can become an application security issue, not just a frontend bug.

React’s own documentation says Server Components can run on a web server during a page request, access the data layer without building a separate API, render before bundling, and pass data or JSX as props to Client Components. That is a major architectural shift from the older model where the browser called an explicit API and the API was clearly the backend boundary. (प्रतिक्रिया) Next.js also documents that the React Server Component payload contains rendered Server Component output, Client Component placeholders, references to client JavaScript files, and any props passed from Server Components to Client Components. (नेक्स्ट.जेएस)

That is the central security reality behind modern frontend frameworks: the attack surface has moved from “JavaScript running in the browser” to “a distributed rendering and mutation system that crosses browser, server, protocol, cache, and build boundaries.”

The old frontend threat model is still useful, but incomplete

The classic React single-page application threat model was easier to draw. The browser loaded static JavaScript, React rendered UI, the app called APIs, and the backend enforced authentication and authorization. The most common frontend security concerns were client-side XSS, unsafe token storage, confused CORS policy, overly permissive postMessage handling, exposed source maps, vulnerable dependencies, and third-party script risk.

That model has not disappeared. A React app that renders untrusted rich text can still introduce XSS. A browser app that stores long-lived tokens in a risky place can still expose sessions. A client that trusts object IDs from the URL can still help attackers find broken object-level authorization in APIs. OWASP’s API Security Top 10 still places Broken Object Level Authorization at the top because attackers can manipulate object IDs in paths, query strings, headers, or payloads to access resources they should not be able to reach. (ओवास्प फाउंडेशन)

The difference is that React and Next.js now allow developers to place sensitive logic in parts of the stack that look like UI code. A .tsx file may fetch privileged data. A component boundary may decide whether a value crosses into the client bundle. A form may call a Server Action without a hand-written REST endpoint. A Proxy file may run before route handling. A cache setting may decide whether a response is reused across users. The security model is no longer “frontend calls backend.” It is “some frontend framework code is backend code.”

Architecture patternWhere code runsCommon security assumptionWhat can go wrongSecurity test that matters
Traditional React SPABrowserBackend APIs are the enforcement pointDOM XSS, token exposure, CORS mistakes, API authorization gapsTest DOM sinks, storage, API auth, role and tenant isolation
React with SSRBrowser and server render processServer rendering is mostly a performance featureHydration edge cases, server-rendered data exposure, cache mistakesCompare HTML and hydrated state across users and roles
Next.js Pages RouterBrowser, Node.js routes, SSR functionsPages and API routes are separate enough to reason aboutAPI route auth drift, SSR data leakage, middleware assumptionsTest API routes and SSR paths independently
Next.js App Router with RSCBrowser, server components, RSC payload, route handlersServer Components are “safe” because they do not ship as client JavaScriptSensitive props, server-only imports, RSC payload disclosure, unsafe serializationInspect what crosses the server-client boundary
Server ActionsBrowser-triggered network calls to server functionsuse server means protected server codeMissing authorization, untrusted arguments, CSRF assumptions, SSRF edge casesTreat each action as a public mutation endpoint
Proxy or MiddlewareBefore route handlingCentralized route protection is enoughAuthorization bypass, matcher drift, internal header trust, incomplete coverageVerify auth again in Server Actions, route handlers, and data access code

The table shows why “frontend framework security” has become a cross-layer discipline. React still renders UI, but React and Next.js also shape server execution, request routing, serialization, and cache behavior.

From SPA Security to Full-Stack Frontend Attack Surface

React reduces ordinary HTML injection risk, but it does not remove XSS

React’s default rendering model is one of the reasons it became popular with security-conscious developers. When developers render ordinary values through JSX, React treats them as text rather than executable HTML. That design helps prevent many basic HTML injection mistakes.

The dangerous part is the phrase “ordinary values.” React still exposes escape hatches, and modern frontend frameworks often use those escape hatches for CMS content, Markdown rendering, WYSIWYG editors, analytics snippets, embedded widgets, and legacy HTML. The official React DOM reference warns that dangerouslySetInnerHTML overrides a DOM node’s आंतरिक HTML and should be used with extreme caution because untrusted HTML can introduce XSS. (प्रतिक्रिया)

Unsafe rich text rendering usually looks like this:

type ArticleBodyProps = {
  htmlFromCms: string;
};

export function ArticleBody({ htmlFromCms }: ArticleBodyProps) {
  return (
    <article
      dangerouslySetInnerHTML={{ __html: htmlFromCms }}
    />
  );
}

The code is short, readable, and dangerous when htmlFromCms can contain attacker-controlled markup. The risk is not that React failed. The risk is that the developer intentionally bypassed React’s safer default rendering path.

A safer pattern is to sanitize before rendering and make the sanitization boundary explicit:

import DOMPurify from "isomorphic-dompurify";

type ArticleBodyProps = {
  htmlFromCms: string;
};

export function ArticleBody({ htmlFromCms }: ArticleBodyProps) {
  const cleanHtml = DOMPurify.sanitize(htmlFromCms, {
    USE_PROFILES: { html: true },
    FORBID_TAGS: ["script", "style", "iframe"],
    FORBID_ATTR: ["onerror", "onload", "onclick"],
  });

  return (
    <article
      dangerouslySetInnerHTML={{ __html: cleanHtml }}
    />
  );
}

Even that pattern should not be treated as magic. Sanitizers need configuration, updates, tests, and realistic malicious fixtures. The safest option is still to avoid raw HTML where possible. If the product requires rich content, the team should treat rich-text rendering as a security-sensitive subsystem with tests, dependency monitoring, and review gates.

The more subtle mistake is assuming that React’s XSS story is only about dangerouslySetInnerHTML. It is not. URL construction, script injection through third-party libraries, unsafe Markdown plugins, DOM-manipulating chart libraries, browser extensions, custom elements, and postMessage bridges can all reopen script execution paths. CSP and Trusted Types can reduce blast radius, but they do not fix unsafe authorization, unsafe serialization, or server-side framework bugs.

Server Components changed the meaning of “frontend code”

Server Components, Server Actions, and Data Boundary Risk

React Server Components changed the security model because the component tree is no longer only a browser-side abstraction. Server Components can execute on the server, fetch data, and pass serializable values into Client Components. In Next.js, app directory components are Server Components by default unless a file uses the "use client" directive. The "use client" directive creates a boundary in the module dependency tree, and code in the Client module subtree is sent to and executed by the browser. (प्रतिक्रिया)

That boundary is where many new frontend framework security bugs begin.

A common unsafe pattern is passing too much data from a Server Component to a Client Component:

// app/account/page.tsx
import AccountPanel from "./AccountPanel";
import { getCurrentUser } from "@/lib/data";

export default async function AccountPage() {
  const user = await getCurrentUser();

  // Dangerous: this may include fields the browser should never receive.
  return <AccountPanel user={user} />;
}
// app/account/AccountPanel.tsx
"use client";

export default function AccountPanel({ user }: { user: any }) {
  return (
    <section>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </section>
  );
}

The Client Component may only display name और email, but the serialized props can still include fields such as internal IDs, role metadata, billing status, feature flags, password reset state, or secrets accidentally attached to the user object. The fix is not to trust React to hide fields. The fix is to create a small data transfer object at the server-client boundary:

// app/account/page.tsx
import "server-only";
import AccountPanel from "./AccountPanel";
import { getCurrentUser } from "@/lib/data";

export default async function AccountPage() {
  const user = await getCurrentUser();

  const publicUser = {
    name: user.name,
    email: user.email,
  };

  return <AccountPanel user={publicUser} />;
}
// app/account/AccountPanel.tsx
"use client";

type PublicUser = {
  name: string;
  email: string;
};

export default function AccountPanel({ user }: { user: PublicUser }) {
  return (
    <section>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </section>
  );
}

Next.js documents this specific class of risk as “environment poisoning”: JavaScript modules can be shared between Server and Client Components, which makes it possible to accidentally import server-only code into the client. The docs recommend using the server-only package to force a build-time error when a server-only module is imported into a Client Component. (नेक्स्ट.जेएस)

React also provides experimental taint APIs for Server Components. The experimental_taintObjectReference API is designed to prevent user data objects from unintentionally reaching the client, while experimental_taintUniqueValue can block particular sensitive values from being passed to client code. React’s own documentation warns that tainting should not be the only security layer because derived values are not automatically blocked in every case. (प्रतिक्रिया)

The engineering rule is simple: never pass database objects, ORM entities, session objects, permission models, or raw API responses directly to Client Components. Build explicit DTOs. Treat the server-client component boundary like an API response.

Server Actions are public mutation surfaces unless proven otherwise

Server Actions are one of the biggest reasons modern frontend frameworks changed the web attack surface. A developer can define an async function with "use server" and call it from a form or client-side transition. That feels like a clean developer experience. It can also hide the fact that the function is reachable through a network request.

React’s documentation is explicit: "use server" marks server-side functions that can be called from client-side code; when called from the client, React sends a network request containing serialized arguments. The same page says arguments to Server Functions are fully client-controlled and must be treated as untrusted input, and that the function must validate whether the logged-in user is allowed to perform the action. (प्रतिक्रिया) React also states that Server Functions are exposed server endpoints when called outside a form. (प्रतिक्रिया)

That means every Server Action should be reviewed like an API endpoint.

Unsafe code often looks like this:

// app/invoices/actions.ts
"use server";

import { db } from "@/lib/db";

export async function deleteInvoice(formData: FormData) {
  const invoiceId = String(formData.get("invoiceId"));

  await db.invoice.delete({
    where: { id: invoiceId },
  });
}

This action is concise but unsafe. It trusts the submitted ID, does not validate the user, does not check ownership, does not check tenant scope, does not rate limit, and does not log a security-relevant mutation.

A safer version makes the security boundary visible:

// app/invoices/actions.ts
"use server";

import "server-only";
import { z } from "zod";
import { db } from "@/lib/db";
import { requireSession } from "@/lib/auth";
import { auditLog } from "@/lib/audit";

const DeleteInvoiceInput = z.object({
  invoiceId: z.string().uuid(),
});

export async function deleteInvoice(formData: FormData) {
  const session = await requireSession();

  const parsed = DeleteInvoiceInput.parse({
    invoiceId: formData.get("invoiceId"),
  });

  const invoice = await db.invoice.findFirst({
    where: {
      id: parsed.invoiceId,
      tenantId: session.tenantId,
    },
    select: {
      id: true,
      tenantId: true,
      createdByUserId: true,
      status: true,
    },
  });

  if (!invoice) {
    await auditLog({
      actorId: session.userId,
      action: "invoice.delete.denied",
      targetId: parsed.invoiceId,
      reason: "not_found_or_wrong_tenant",
    });

    throw new Error("Invoice not found");
  }

  if (!session.permissions.includes("invoice:delete")) {
    await auditLog({
      actorId: session.userId,
      action: "invoice.delete.denied",
      targetId: invoice.id,
      reason: "missing_permission",
    });

    throw new Error("Not authorized");
  }

  await db.invoice.delete({
    where: {
      id: invoice.id,
    },
  });

  await auditLog({
    actorId: session.userId,
    action: "invoice.delete.allowed",
    targetId: invoice.id,
  });
}

The important difference is not the validation library. It is the placement of authorization. The action validates the user, tenant, object ownership, permission, and mutation inside the server-side function that performs the state change. That is the pattern security teams should require for Server Actions.

Next.js adds built-in Server Actions security features, including an Origin-to-Host CSRF check, a default 1 MB body size limit for action requests, encrypted action IDs, and dead code elimination for unused Server Functions. The same docs say allowedOrigins should be configured when safe proxy or CDN domains must invoke Server Actions. (नेक्स्ट.जेएस) Those framework features help, but they do not replace application-level authorization.

Proxy and Middleware are not enough for authorization

Next.js 16 renamed Middleware to Proxy to better describe its purpose, while keeping the function of running code before a request is completed. The official docs describe Proxy as a way to rewrite, redirect, modify request or response headers, or respond before route handling. (नेक्स्ट.जेएस) That is useful for routing, localization, coarse request handling, and some entry checks. It is not a sufficient authorization model by itself.

Next.js documentation now says a matcher change or refactor that moves a Server Function to a different route can silently remove Proxy coverage, and it explicitly recommends verifying authentication and authorization inside each Server Function rather than relying on Proxy alone. (नेक्स्ट.जेएस) The production checklist also recommends verifying authentication and authorization inside each Server Action, not relying only on Proxy, layout, or page-level checks. (नेक्स्ट.जेएस)

CVE-2025-29927 showed why this matters. The GitHub advisory for the vulnerability states that authorization checks within a Next.js application could be bypassed if the authorization check occurred in middleware. Affected versions included Next.js 12.x before 12.3.5, 13.x before 13.5.9, 14.x before 14.2.25, and 15.x before 15.2.3, and the recommended workaround when patching was not possible was to block external user requests containing the x-middleware-subrequest header. (गिटहब) NVD describes the same issue as an authorization bypass when checks occur in middleware. (एनवीडी)

The lesson is not “never use Proxy.” The lesson is “do not place the only authorization decision in a layer that can be skipped, misconfigured, mismatched, or bypassed by a framework bug.”

A safer pattern is layered enforcement:

// proxy.ts
import { NextRequest, NextResponse } from "next/server";

export function proxy(req: NextRequest) {
  const sessionCookie = req.cookies.get("session")?.value;

  if (!sessionCookie && req.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", req.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

The Proxy check improves user flow and blocks obvious unauthenticated navigation. It should not be the last line of defense.

// app/dashboard/actions.ts
"use server";

import { requireSession } from "@/lib/auth";
import { canUpdateProject } from "@/lib/authorization";

export async function updateProjectName(projectId: string, name: string) {
  const session = await requireSession();

  const allowed = await canUpdateProject({
    userId: session.userId,
    tenantId: session.tenantId,
    projectId,
  });

  if (!allowed) {
    throw new Error("Not authorized");
  }

  // Perform mutation only after object-level authorization.
}

The application still checks authorization where the mutation occurs. That is what survives route refactors, matcher mistakes, and framework-layer issues.

React2Shell made the RSC protocol a board-level patching issue

CVE-2025-55182, widely referred to as React2Shell, changed how security teams think about frontend frameworks. React’s official advisory described a critical vulnerability in React Server Components, rated CVSS 10.0, affecting React 19.0, 19.1.0, 19.1.1, and 19.2.0 packages such as react-server-dom-webpack, react-server-dom-parcel, और react-server-dom-turbopack. React warned that an app could still be vulnerable if it supported React Server Components, even if it did not implement Server Function endpoints. (प्रतिक्रिया) NVD describes CVE-2025-55182 as a pre-authentication remote code execution vulnerability in React Server Components caused by unsafe deserialization of payloads from HTTP requests to Server Function endpoints. (एनवीडी)

Next.js published a downstream advisory for CVE-2025-66478, stating that the React Server Components protocol issue could allow remote code execution when processing attacker-controlled requests in unpatched environments. (नेक्स्ट.जेएस) The practical lesson is straightforward: RSC is not just a rendering optimization. It is a protocol boundary that processes attacker-controlled HTTP traffic in some configurations.

That is a different class of risk from DOM XSS. XSS executes in the browser under the victim’s session. React2Shell-style flaws threaten server-side execution. They can expose environment variables, server files, credentials, internal network access, and downstream cloud resources depending on deployment context. The exact exploitability depends on versions, framework integration, routing, hosting, and mitigations, but the category is clear: a frontend framework protocol can become a server compromise path.

The December 2025 React and Next.js security sequence also showed that the risk did not end with one RCE. Next.js later announced two additional RSC-related vulnerabilities: a denial-of-service issue tracked as CVE-2025-55184, later requiring a complete fix under CVE-2025-67779, and a source code exposure issue tracked as CVE-2025-55183. The Next.js advisory said affected applications were those using React Server Components with the App Router and provided patched version lines across Next.js 14, 15, and 16. (नेक्स्ट.जेएस)

For security teams, the most important operational change is inventory. It is no longer enough to ask whether the company uses React. Teams need to know which applications use Next.js App Router, which versions of react, react-dom, और react-server-dom-* packages are present, which hosting models are used, whether Server Actions are enabled, and whether exposure differs between self-hosted, Vercel-hosted, edge, container, and serverless deployments.

A minimal inventory command sequence looks like this:

npm ls next react react-dom react-server-dom-webpack react-server-dom-turbopack react-server-dom-parcel

npm audit --json > npm-audit.json

node -e "const p=require('./package.json'); console.log(p.dependencies, p.devDependencies)"

For pnpm workspaces:

pnpm -r list next react react-dom react-server-dom-webpack react-server-dom-turbopack react-server-dom-parcel

pnpm audit --json > pnpm-audit.json

npm audit submits dependency information to the configured registry and asks for known vulnerability reports, while pnpm audit checks installed packages for known security issues and recommends updates or overrides when needed. (एनपीएम दस्तावेज़) These tools are useful for known CVEs. They do not prove that Server Actions are authorized correctly, that RSC payloads do not leak sensitive props, or that caches are scoped safely.

Server Actions also created SSRF exposure under specific conditions

CVE-2024-34351 is another useful case because it links a modern frontend framework feature to a classic server-side vulnerability class. NVD describes CVE-2024-34351 as a Server-Side Request Forgery vulnerability in Next.js Server Actions. The required conditions were specific: the app was self-hosted, used Server Actions, and the Server Action performed a redirect to a relative path starting with /; under those conditions, a modified Host header could make requests appear to originate from the Next.js application server. The issue was fixed in Next.js 14.1.1. (एनवीडी) Assetnote’s advisory also stated that successful exploitation could let an attacker read the full HTTP response. (Assetnote)

This CVE matters because it demonstrates a pattern that security teams should expect more often: framework convenience features create server-side behaviors that do not look like traditional backend code. A redirect inside a Server Action can become part of an SSRF chain. A header trusted by framework internals can influence server behavior. A self-hosted deployment can differ materially from a managed platform deployment.

OWASP’s SSRF guidance recommends validating all client-supplied input, enforcing URL scheme, port, and destination with a positive allowlist, avoiding raw response forwarding, disabling redirects where possible, and accounting for DNS rebinding and time-of-check/time-of-use issues. (ओवास्प फाउंडेशन) Those principles apply even when the SSRF entry point is hidden inside what product teams think of as frontend framework code.

सीवीईFramework areaAttack conditionप्रभावMain mitigationWhat to test
CVE-2024-34351Next.js Server ActionsSelf-hosted Next.js, Server Actions, relative redirect, attacker-controlled Host headerSSRF with server-originated requestsUpgrade to Next.js 14.1.1 or laterHost header handling, relative redirects, outbound request logs
CVE-2025-29927Next.js Middleware or ProxyAuthorization exists only in middleware on vulnerable versionsAuthorization bypassUpgrade to fixed versions, block x-middleware-subrequest externally if neededProtected routes with and without middleware coverage, app-level auth checks
सीवीई-2025-55182React Server Components protocolAffected React 19 RSC packages processing attacker-controlled requestsPre-authentication RCEUpgrade React and affected RSC packages to patched versionsPackage inventory, RSC exposure, server logs, suspicious action requests
सीवीई-2025-66478Next.js downstream RSC impactNext.js App Router using vulnerable RSC protocol componentsRCE in unpatched environmentsFollow Next.js patched versions and official fix guidanceNext.js version, App Router usage, exposure by deployment model
CVE-2025-55183RSC source code exposureAffected RSC implementations after React2Shell patch reviewPossible compiled Server Function source exposureUpgrade to patched framework versionsUnexpected source disclosure, function output anomalies
CVE-2025-55184 and CVE-2025-67779RSC denial of serviceCrafted request causing server hang or resource exhaustionAvailability impactUpgrade to complete patched versionsHigh-latency requests, event-loop stalls, abnormal RSC traffic

The pattern across the table is more important than any single CVE. Modern frontend frameworks have security-sensitive internal protocols, request headers, routing layers, serialized payloads, and server execution paths. They deserve the same treatment as API gateways, backend frameworks, and application servers.

Data leakage often happens at the boundary, not in the database

Most React and Next.js data leaks are not caused by someone writing SELECT * FROM users in public code and knowingly exposing it. They happen when a data object crosses the wrong boundary. In Server Components, that boundary may be the props passed into a Client Component. In Server Actions, it may be the return value serialized to the client. In route handlers, it may be an error response. In caching, it may be a private response stored as if it were public.

The easiest way to prevent this class of bug is to make boundary objects boring. Do not pass ORM models. Do not pass raw user objects. Do not pass permission maps. Do not pass session objects. Do not pass internal workflow state. Create small response shapes that contain only fields the browser needs.

Unsafe:

return <AdminUserCard user={userFromDatabase} />;

Better:

return (
  <AdminUserCard
    user={{
      id: userFromDatabase.id,
      displayName: userFromDatabase.displayName,
      avatarUrl: userFromDatabase.avatarUrl,
      accountStatus: userFromDatabase.accountStatus,
    }}
  />
);

Best for larger apps:

type AdminUserCardDTO = {
  id: string;
  displayName: string;
  avatarUrl: string | null;
  accountStatus: "active" | "suspended" | "pending";
};

function toAdminUserCardDTO(user: UserRecord): AdminUserCardDTO {
  return {
    id: user.id,
    displayName: user.displayName,
    avatarUrl: user.avatarUrl,
    accountStatus: user.accountStatus,
  };
}

Then test that DTOs do not grow accidentally:

import { expect, test } from "vitest";
import { toAdminUserCardDTO } from "./dto";

test("AdminUserCardDTO does not expose sensitive user fields", () => {
  const dto = toAdminUserCardDTO({
    id: "u_123",
    displayName: "Ada",
    avatarUrl: null,
    accountStatus: "active",
    email: "ada@example.com",
    passwordHash: "never",
    mfaSecret: "never",
    internalRiskScore: 9001,
  } as any);

  expect(dto).toEqual({
    id: "u_123",
    displayName: "Ada",
    avatarUrl: null,
    accountStatus: "active",
  });

  expect(JSON.stringify(dto)).not.toContain("passwordHash");
  expect(JSON.stringify(dto)).not.toContain("mfaSecret");
  expect(JSON.stringify(dto)).not.toContain("internalRiskScore");
});

This kind of test is not glamorous, but it catches the mistakes that frameworks cannot reliably prevent.

Cache behavior can turn a small auth bug into a wide data leak

Caching is where modern frontend frameworks become especially tricky. A route may include static shell output, dynamic server-rendered data, prefetched RSC payloads, CDN behavior, browser caching, framework fetch caching, and revalidation rules. The security question is not just “does this route require login?” It is also “can a response produced for one user be reused for another user?”

The risk is highest when authenticated content is mixed with aggressive caching. A user-specific dashboard, billing page, admin view, cart, onboarding state, or tenant-specific route should not be cached as public content. For sensitive responses, teams should be explicit:

// app/api/account/route.ts
export async function GET() {
  const data = await getPrivateAccountData();

  return Response.json(data, {
    headers: {
      "Cache-Control": "private, no-store",
    },
  });
}

For server-side fetches that must be request-specific:

const account = await fetch("https://api.example.com/account", {
  headers: {
    Authorization: `Bearer ${session.accessToken}`,
  },
  cache: "no-store",
});

For route handlers that return tenant-scoped data, vary behavior must be intentional. A CDN or reverse proxy should not decide that two authenticated responses are equivalent because the URL is the same.

return Response.json(projects, {
  headers: {
    "Cache-Control": "private, no-store",
    "Vary": "Cookie, Authorization",
  },
});

The best test is a two-user comparison. Log in as User A and User B in different tenants. Request the same route. Compare HTML, RSC payloads, API responses, and cache headers. Then repeat with different roles. If private data appears in shared cacheable responses, the bug is already serious even if no exploit has been automated.

CSP is a defense layer, not a security model

Content Security Policy is still valuable for React and Next.js applications. Next.js documentation states that CSP helps guard against XSS, clickjacking, and other code injection attacks by specifying permitted origins for scripts, stylesheets, images, fonts, frames, and other content sources. (नेक्स्ट.जेएस) MDN documents सख्त-गतिशील as a source expression that propagates trust from a nonce- or hash-bearing root script to scripts it loads, while ignoring allowlists such as 'self' in supporting browsers. (MDN वेब दस्तावेज़)

A strict CSP can reduce the impact of a missed XSS sink, but it should not be treated as a substitute for safe rendering. CSP also has trade-offs in Next.js. Nonce-based CSP often requires dynamic rendering because each response needs a fresh nonce. Third-party scripts, analytics, tag managers, and inline framework scripts can make policies harder to maintain. The security goal is not to copy a policy from a blog post. It is to design a policy that matches the rendering mode and script model of the application.

A simplified CSP header might look like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';
  connect-src 'self' https://api.example.com;
  upgrade-insecure-requests;

For a Next.js application, CSP should be tested in report-only mode first:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  report-uri /api/csp-report;

A good CSP program answers three questions:

  1. Which script execution paths are intentionally allowed?
  2. Which third-party scripts are actually necessary?
  3. Which violations indicate real injection attempts rather than broken analytics?

Trusted Types can add another layer for DOM XSS by requiring dangerous DOM sinks to receive trusted objects rather than raw strings. That is particularly useful in large React applications where third-party components or legacy DOM code may use unsafe sinks. Still, Trusted Types and CSP do not fix missing Server Action authorization, SSRF, middleware bypass, or dependency vulnerabilities.

Supply chain risk is part of frontend framework security

Frontend frameworks sit on top of a large JavaScript supply chain. React and Next.js applications often depend on build tools, bundlers, transpilers, lint plugins, test tools, Markdown processors, syntax highlighters, image libraries, analytics SDKs, authentication SDKs, UI kits, date libraries, charting libraries, and deployment adapters. Some dependencies run in the browser. Some run at build time. Some run on the server. Some run in CI with access to secrets.

That makes dependency governance part of the attack surface.

At minimum, teams should know:

# Show direct and transitive framework versions.
npm ls next react react-dom

# Check known advisories.
npm audit

# Produce machine-readable audit output for CI.
npm audit --json

# For pnpm workspaces.
pnpm -r list next react react-dom
pnpm audit --json

GitHub Dependabot can alert repositories affected by newly published GitHub Security Advisories, and GitHub documents that those alerts can lead to security updates. (GitHub दस्तावेज़) That helps with known vulnerable packages. It does not solve the harder problems: malicious package behavior before detection, compromised maintainer accounts, risky postinstall scripts, typosquatting, unsafe build plugins, source map leakage, and framework-specific logic flaws.

A practical frontend framework supply chain policy should include:

Controlयह क्यों मायने रखती हैHow to enforce it
Lockfiles committed and reviewedPrevents surprise transitive dependency driftRequire lockfile diffs in pull requests
Framework version inventoryReact and Next.js CVEs depend on exact versionsCI job prints npm ls next react react-dom
Dependency alertsCatches known vulnerable packagesEnable Dependabot or equivalent
Postinstall script reviewBuild scripts can run in CI with secretsUse package manager settings and review new packages
Source map policyPublic source maps can expose internal code structurePublish only intended source maps and restrict access
Environment variable reviewClient-exposed variables can leak secretsAllow only intentional NEXT_PUBLIC_ variables
Build artifact scanningSecrets can enter bundles or .next outputScan build artifacts before deployment
Third-party script inventoryScripts can bypass app code controlsRequire owner, purpose, CSP entry, and renewal date

The frontend ecosystem moves fast. Security teams need update velocity, but they also need controlled updates. A blind npm update across a large app can break production. A frozen lockfile can leave critical RSC vulnerabilities unpatched. The right workflow is routine small updates, emergency patch lanes for critical CVEs, automated tests, and a rollback plan.

How attackers test modern React and Next.js apps

A realistic attacker does not care whether a bug lives in “frontend” or “backend.” They care whether a request produces unauthorized data, state change, server execution, SSRF, cache poisoning, or credential exposure. For modern frontend frameworks, the testing path often looks like this:

कदमWhat the tester looks forयह क्यों मायने रखती है
Fingerprint framework behaviorNext.js headers, RSC requests, route patterns, build artifactsDetermines whether App Router, RSC, or Server Actions may be present
Enumerate public routesPages, route handlers, static assets, API-like pathsFinds unprotected entry points
Compare roles and tenantsSame route across different usersDetects BOLA, IDOR, cache leakage, and missing authorization
Inspect client bundlesClient-side secrets, route names, action references, source mapsReveals hidden routes or leaked configuration
Test Server Actions as endpointsMutations with modified IDs and form fieldsFinds missing action-level authorization
Test Proxy assumptionsRoutes that should be protected but are notFinds matcher drift and middleware-only auth
Review RSC payloadsSerialized props and component dataFinds sensitive data crossing the client boundary
Test Host and forwarded headersHost, X-Forwarded-Host, Origin, schemeFinds SSRF, redirect, CSRF, and proxy trust issues
Check cache isolationResponse reuse across usersFinds cross-user data exposure
Review dependency versionsKnown CVEs and vulnerable framework packagesFinds patch gaps

A safe validation script for basic framework and cache signals might look like this:

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="${1:-https://example.com}"

echo "[*] Checking basic headers"
curl -sI "$BASE_URL" | sed -n '1,40p'

echo
echo "[*] Checking cache headers on authenticated-sensitive paths"
for path in "/dashboard" "/account" "/settings" "/billing"; do
  echo "----- $path"
  curl -sI "$BASE_URL$path" | grep -iE 'cache-control|vary|set-cookie|x-middleware|x-nextjs|content-type' || true
done

echo
echo "[*] Checking unusual Host handling on a harmless path"
curl -sI "$BASE_URL/" \
  -H "Host: attacker.invalid" \
  -H "Origin: https://attacker.invalid" \
  | sed -n '1,30p'

This is not an exploit. It is a sanity check. The output tells a tester whether sensitive routes have cache headers, whether the app reacts strangely to Host or Origin changes, and whether framework-specific headers are visible.

Server Action testing should be scoped, logged, and performed only with permission. The core idea is to treat each action like an API mutation: change object IDs, tenant IDs, role-dependent fields, hidden form values, and replay timing. If a low-privileged user can mutate another user’s object by changing a serialized argument, the bug is authorization, not “frontend.”

For teams that need repeatable evidence across large React and Next.js estates, an AI-assisted testing workflow can help connect route discovery, action mapping, request replay, response comparison, and report generation. Penligent’s authorized testing workflow is relevant here because modern frontend framework issues often require multiple observations rather than one scanner signature: a route map, user roles, framework behavior, request mutations, response differences, and proof that a finding is reproducible. Penligent’s site describes it as an AI-powered penetration testing tool for authorized security testing, and its React2Shell write-up is a useful example of treating RSC issues as operational exposure rather than isolated CVE trivia. (पेनलिजेंट)

Defensive patterns that actually hold up

The strongest frontend framework security programs are boring in the right places. They do not rely on a single clever control. They enforce authorization repeatedly, minimize data crossing boundaries, patch quickly, and test behavior from the outside.

Put authorization next to the data and mutation

Route-level checks are useful, but object-level authorization belongs near the data access or mutation. A user should not be able to update an invoice merely because they reached a page that contains an update form. The Server Action, Route Handler, or data access function must verify session, tenant, role, and object ownership.

A reusable authorization helper can make this less painful:

// lib/authorization.ts
import "server-only";
import { db } from "@/lib/db";

type ProjectAccessInput = {
  userId: string;
  tenantId: string;
  projectId: string;
  permission: "project:read" | "project:update" | "project:delete";
};

export async function requireProjectAccess(input: ProjectAccessInput) {
  const membership = await db.projectMember.findFirst({
    where: {
      userId: input.userId,
      tenantId: input.tenantId,
      projectId: input.projectId,
      permissions: {
        has: input.permission,
      },
    },
    select: { id: true },
  });

  if (!membership) {
    throw new Error("Not authorized");
  }
}

Use it inside every sensitive mutation:

"use server";

import { requireSession } from "@/lib/auth";
import { requireProjectAccess } from "@/lib/authorization";

export async function renameProject(projectId: string, newName: string) {
  const session = await requireSession();

  await requireProjectAccess({
    userId: session.userId,
    tenantId: session.tenantId,
    projectId,
    permission: "project:update",
  });

  // Safe to continue with mutation.
}

Minimize what crosses into the client

Every Server Component to Client Component boundary should be reviewed like an API response. The RSC payload can include props passed into Client Components. That means “not visible in JSX” is not the same as “not sent to the browser.” Use DTOs, server-only, tests, and taint APIs where appropriate.

Treat Server Actions as hostile input handlers

Every Server Action should validate input shape, input size, user identity, tenant scope, object ownership, mutation permission, and rate limits. React’s documentation is clear that Server Function arguments are client-controlled and untrusted. (प्रतिक्रिया) Next.js’s Server Actions docs describe built-in CSRF checks and body size limits, but application permission checks remain the developer’s job. (नेक्स्ट.जेएस)

Patch framework protocol vulnerabilities quickly

React2Shell and the follow-on RSC issues showed that React and Next.js version inventory is now critical. A frontend dependency can be a server-side RCE dependency. Emergency patching must include application rebuilds and redeployments, not just package file changes.

A CI check can fail builds when framework versions fall below a required floor:

// scripts/check-framework-versions.js
const semver = require("semver");
const pkg = require("../package.json");

const deps = {
  ...pkg.dependencies,
  ...pkg.devDependencies,
};

const required = {
  next: ">=15.2.8",
  react: ">=19.1.2",
  "react-dom": ">=19.1.2",
};

for (const [name, range] of Object.entries(required)) {
  const installed = deps[name];

  if (!installed) {
    console.error(`Missing dependency: ${name}`);
    process.exit(1);
  }

  const cleaned = installed.replace(/^[^\d]*/, "");

  if (!semver.satisfies(cleaned, range)) {
    console.error(`${name}@${installed} does not satisfy ${range}`);
    process.exit(1);
  }
}

console.log("Framework dependency floors satisfied.");

Version floors must be maintained by the security or platform team based on current advisories. The example is a pattern, not a universal policy.

Block untrusted internal framework headers at the edge

CVE-2025-29927 made one point painfully clear: internal framework headers should not be accepted blindly from the public internet. If a framework uses internal request headers, the edge should strip or block them unless there is a documented reason to allow them.

For Nginx, a defensive rule might look like this:

# Reject public requests containing a Next.js internal middleware header.
if ($http_x_middleware_subrequest != "") {
  return 403;
}

For an application-level check:

export function rejectInternalFrameworkHeaders(headers: Headers) {
  const blocked = [
    "x-middleware-subrequest",
    "x-middleware-subrequest-id",
  ];

  for (const name of blocked) {
    if (headers.has(name)) {
      throw new Error(`Blocked internal framework header: ${name}`);
    }
  }
}

Header filtering is not a replacement for patching. It is a compensating control when public traffic should never be allowed to set framework-internal headers.

Test cache isolation before every major release

Automated tests should compare responses across users, roles, and tenants. A simple Playwright pattern can catch obvious cross-user leaks:

import { test, expect } from "@playwright/test";

test("dashboard does not leak data between tenants", async ({ browser }) => {
  const contextA = await browser.newContext({ storageState: "tenant-a.json" });
  const contextB = await browser.newContext({ storageState: "tenant-b.json" });

  const pageA = await contextA.newPage();
  const pageB = await contextB.newPage();

  await pageA.goto("/dashboard");
  await pageB.goto("/dashboard");

  const htmlA = await pageA.content();
  const htmlB = await pageB.content();

  expect(htmlA).toContain("Tenant A");
  expect(htmlA).not.toContain("Tenant B");

  expect(htmlB).toContain("Tenant B");
  expect(htmlB).not.toContain("Tenant A");
});

This does not prove perfect isolation, but it catches the type of cache and rendering mistakes that can become severe in a server-rendered app.

A practical secure review checklist for React and Next.js

Security review for modern frontend frameworks should be specific enough to map to code. A generic “check auth” ticket is not enough.

AreaReview questionBad signSafer pattern
Server ComponentsAre raw database objects passed to Client Components?return <Client user={user} /> with full ORM objectDTO with explicit public fields
Client boundaryAre server-only modules importable from client code?नहीं server-only in data access modulesimport "server-only" in server data modules
Server ActionsDoes every action validate session and authorization internally?Action trusts page-level authAction checks session, tenant, object ownership, permission
ProxyIs Proxy the only auth layer?Protected route depends only on matcherProxy for routing, app logic for authorization
RSC versionsAre React and Next.js versions patched for RSC advisories?Unknown versions across appsCentral inventory and emergency patch lanes
CacheCan private pages be cached publicly?लापता कैश-नियंत्रण on account routesprivate, no-store for user-specific responses
Rich HTMLIs untrusted HTML rendered directly?Raw dangerouslySetInnerHTMLSanitizer, allowlist, tests, CSP
CSPIs CSP absent or too permissive?असुरक्षित-इनलाइन everywhere without reviewnonce or hash policy where feasible
HeadersAre Host, Origin, and forwarded headers trusted blindly?Redirects or URLs built from raw Hostallowlisted origins and trusted proxy config
DependenciesAre framework CVEs handled manually and slowly?No alerting or lockfile reviewDependabot, audit, version floors, CI gates

A useful review also includes log questions. Can the team see failed Server Action authorization attempts? Can it distinguish unauthenticated probes from normal form submissions? Are Host header anomalies logged? Are cache hits visible at the CDN? Are RSC request paths observable? If the answer is no, incident response becomes guesswork.

Safe testing commands for defenders

The following commands are meant for systems you own or are authorized to test.

Check versions:

npm ls next react react-dom react-server-dom-webpack react-server-dom-turbopack react-server-dom-parcel

Check known advisories:

npm audit
pnpm audit

Check whether sensitive routes are cacheable:

for path in /account /dashboard /settings /billing; do
  echo "----- $path"
  curl -sI "https://app.example.com$path" \
    | grep -iE 'cache-control|vary|set-cookie|content-type'
done

Check whether public traffic can set internal framework headers:

curl -sI "https://app.example.com/dashboard" \
  -H "x-middleware-subrequest: middleware" \
  | sed -n '1,20p'

A secure, patched, defense-in-depth deployment should not grant access merely because a request includes a framework-internal header. For vulnerable versions, the official workaround for CVE-2025-29927 was to prevent external user requests containing x-middleware-subrequest from reaching the application. (गिटहब)

Check Host and Origin handling:

curl -sI "https://app.example.com/" \
  -H "Host: attacker.example" \
  -H "Origin: https://attacker.example" \
  | sed -n '1,30p'

Unexpected redirects, server-side fetches, or permissive action behavior deserve investigation.

Check whether a client bundle contains obvious secrets:

grep -RniE "AWS_SECRET|DATABASE_URL|PRIVATE_KEY|BEGIN RSA|sk_live|ghp_" .next/static 2>/dev/null || true

This is a blunt instrument, but it catches embarrassing mistakes. A more mature pipeline scans all build artifacts and source maps before deployment.

How frontend framework security should change team workflow

The biggest organizational mistake is splitting responsibility by old labels. Product engineers own components. Backend engineers own APIs. Platform engineers own deployment. Security engineers own testing. In a modern React or Next.js application, all of those responsibilities meet in the same route.

A Server Component may be owned by the product team but execute privileged data access. A Server Action may be written by a frontend engineer but mutate database state. A Proxy rule may be owned by the platform team but affect authorization. A dependency alert may look like a frontend package update but patch a server-side RCE. A CDN rule may be configured outside the repo but decide whether authenticated RSC payloads are cached.

Security programs should therefore add framework-aware review gates:

  1. Any Server Action that mutates state requires authorization review.
  2. Any Client Component receiving server data requires DTO review.
  3. Any route containing user-specific data requires cache review.
  4. Any use of dangerouslySetInnerHTML requires sanitizer and CSP review.
  5. Any framework major upgrade requires security regression tests.
  6. Any critical React or Next.js advisory triggers inventory, patch, deploy, and validation.
  7. Any Proxy or Middleware auth logic requires duplicate enforcement in application logic.

This is not bureaucracy for its own sake. It is a response to how frontend frameworks now work.

अक्सर पूछे जाने वाले प्रश्न

Are frontend frameworks now part of the backend attack surface?

Yes, for modern React and Next.js applications.

  • Server Components can execute on the server and access the data layer.
  • Server Actions can perform server-side mutations from client-triggered requests.
  • Proxy or Middleware can influence routing and authorization flow before route handling.
  • RSC payloads serialize server-rendered component data for the client.
  • Framework dependencies such as React, Next.js, and react-server-dom-* packages can carry server-side vulnerabilities.

The practical takeaway is that frontend framework security should be reviewed with the same seriousness as backend framework security.

Does React prevent XSS by default?

React reduces many common XSS risks, but it does not eliminate XSS.

  • Ordinary JSX value rendering is safer than manual HTML string construction.
  • dangerouslySetInnerHTML bypasses React’s safer default and can introduce XSS if the HTML is untrusted.
  • Unsafe Markdown plugins, third-party widgets, DOM libraries, URL construction, and postMessage code can still create script execution paths.
  • CSP and Trusted Types can reduce blast radius, but they do not replace safe rendering and sanitization.

Are Server Components safer because they run on the server?

Server Components remove some browser-side exposure, but they create a different boundary.

  • Server-only code is not automatically safe if sensitive values are passed to Client Components.
  • Props passed from Server Components to Client Components can be serialized into the RSC payload.
  • Raw database objects should not be passed across the server-client boundary.
  • Use DTOs, server-only, strict typing, and tests to limit what reaches the browser.

Server Components are powerful, not automatically harmless.

Should Next.js Proxy or Middleware be used for authentication?

Proxy can be used as an entry check, but it should not be the only authorization layer.

  • Proxy is useful for redirects, coarse route gating, localization, and request shaping.
  • Object-level authorization should live inside Server Actions, Route Handlers, or the data access layer.
  • CVE-2025-29927 showed the risk of relying only on middleware-based authorization in vulnerable Next.js versions.
  • Next.js documentation recommends verifying authorization inside each Server Function rather than relying on Proxy alone.

How should teams test Server Actions?

Treat every Server Action as a public mutation endpoint.

  • Validate all arguments as untrusted input.
  • Test whether a lower-privileged user can modify another user’s object ID.
  • Test tenant isolation, role boundaries, hidden form fields, replay behavior, and body size limits.
  • Verify that authorization is inside the action, not only in the page, layout, Proxy, or UI.
  • Log denied attempts so suspicious mutation probes can be investigated.

What did React2Shell change about frontend framework security?

React2Shell made it clear that frontend framework internals can become server-side critical vulnerabilities.

  • CVE-2025-55182 affected React Server Components packages and was rated CVSS 10.0 by React.
  • NVD describes the issue as pre-authentication remote code execution caused by unsafe deserialization of attacker-controlled payloads.
  • Next.js published downstream guidance for App Router users.
  • Security teams now need version inventory for React, Next.js, and RSC-related packages, not just browser-side scanning.

Is CSP enough to protect a React or Next.js application?

No. CSP is a valuable defense layer, not a complete model.

  • CSP can reduce the impact of script injection.
  • Nonce-based CSP can be effective but may affect rendering and caching choices.
  • CSP does not fix missing Server Action authorization.
  • CSP does not fix SSRF, middleware bypass, dependency CVEs, or sensitive RSC props.
  • Use CSP alongside safe rendering, strict boundaries, dependency updates, logging, and authorization checks.

What should security teams patch first?

Prioritize by exploitability and blast radius.

  • Patch critical React and Next.js RSC vulnerabilities first, especially RCE-class issues.
  • Patch Next.js versions affected by middleware authorization bypass if the app uses Middleware or Proxy for access control.
  • Patch Server Actions SSRF issues if the app is self-hosted and uses affected patterns.
  • Review Server Actions that mutate sensitive data.
  • Review routes that serve authenticated or tenant-specific content with ambiguous cache headers.
  • Remove or harden unsafe rich HTML rendering.

The security boundary moved, so the testing model must move too

React, Next.js, and Server Components did not make frontend development insecure by default. They made the boundary more powerful and more implicit. That is where risk lives. A component may be UI, server code, data access, serialization source, cache participant, and mutation trigger depending on where it runs and how it is imported.

The right response is not to avoid modern frontend frameworks. The right response is to stop treating them as only frontend. Patch them like backend dependencies. Test Server Actions like APIs. Review RSC payloads like response bodies. Treat Proxy as a routing layer, not a complete authorization system. Keep sensitive data out of Client Component props. Use CSP as a backup, not a crutch. Build cache rules that assume authenticated data is dangerous by default.

Frontend frameworks changed the attack surface because they changed where application logic lives. Security teams that update their model will find real bugs earlier. Teams that keep the old browser-only model will keep missing the parts of the app that now matter most.

पोस्ट साझा करें:
संबंधित पोस्ट
hi_INHindi