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. (مؤسسة OWASP)
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 pattern | Where code runs | Common security assumption | What can go wrong | Security test that matters |
|---|---|---|---|---|
| Traditional React SPA | Browser | Backend APIs are the enforcement point | DOM XSS, token exposure, CORS mistakes, API authorization gaps | Test DOM sinks, storage, API auth, role and tenant isolation |
| React with SSR | Browser and server render process | Server rendering is mostly a performance feature | Hydration edge cases, server-rendered data exposure, cache mistakes | Compare HTML and hydrated state across users and roles |
| Next.js Pages Router | Browser, Node.js routes, SSR functions | Pages and API routes are separate enough to reason about | API route auth drift, SSR data leakage, middleware assumptions | Test API routes and SSR paths independently |
| Next.js App Router with RSC | Browser, server components, RSC payload, route handlers | Server Components are “safe” because they do not ship as client JavaScript | Sensitive props, server-only imports, RSC payload disclosure, unsafe serialization | Inspect what crosses the server-client boundary |
| إجراءات الخادم | Browser-triggered network calls to server functions | use server means protected server code | Missing authorization, untrusted arguments, CSRF assumptions, SSRF edge cases | Treat each action as a public mutation endpoint |
| Proxy or Middleware | Before route handling | Centralized route protection is enough | Authorization bypass, matcher drift, internal header trust, incomplete coverage | Verify 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.

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 بشكل خطيرSetSetInnerHTML 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 بشكل خطيرSetSetInnerHTML. 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”

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 الاسم و البريد الإلكتروني, 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. (NVD)
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-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. (NVD)
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. (npm Docs) 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 المضيف header could make requests appear to originate from the Next.js application server. The issue was fixed in Next.js 14.1.1. (NVD) 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. (مؤسسة OWASP) Those principles apply even when the SSRF entry point is hidden inside what product teams think of as frontend framework code.
| مكافحة التطرف العنيف | Framework area | Attack condition | التأثير | Main mitigation | What to test |
|---|---|---|---|---|---|
| CVE-2024-34351 | Next.js Server Actions | Self-hosted Next.js, Server Actions, relative redirect, attacker-controlled Host header | SSRF with server-originated requests | Upgrade to Next.js 14.1.1 or later | Host header handling, relative redirects, outbound request logs |
| CVE-2025-29927 | Next.js Middleware or Proxy | Authorization exists only in middleware on vulnerable versions | Authorization bypass | Upgrade to fixed versions, block x-middleware-subrequest externally if needed | Protected routes with and without middleware coverage, app-level auth checks |
| CVE-2025-55182 | React Server Components protocol | Affected React 19 RSC packages processing attacker-controlled requests | Pre-authentication RCE | Upgrade React and affected RSC packages to patched versions | Package inventory, RSC exposure, server logs, suspicious action requests |
| CVE-2025-66478 | Next.js downstream RSC impact | Next.js App Router using vulnerable RSC protocol components | RCE in unpatched environments | Follow Next.js patched versions and official fix guidance | Next.js version, App Router usage, exposure by deployment model |
| CVE-2025-55183 | RSC source code exposure | Affected RSC implementations after React2Shell patch review | Possible compiled Server Function source exposure | Upgrade to patched framework versions | Unexpected source disclosure, function output anomalies |
| CVE-2025-55184 and CVE-2025-67779 | RSC denial of service | Crafted request causing server hang or resource exhaustion | Availability impact | Upgrade to complete patched versions | High-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.
غير آمن:
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:
- Which script execution paths are intentionally allowed?
- Which third-party scripts are actually necessary?
- 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:
| التحكم | ما أهمية ذلك | How to enforce it |
|---|---|---|
| Lockfiles committed and reviewed | Prevents surprise transitive dependency drift | Require lockfile diffs in pull requests |
| Framework version inventory | React and Next.js CVEs depend on exact versions | CI job prints npm ls next react react-dom |
| Dependency alerts | Catches known vulnerable packages | Enable Dependabot or equivalent |
| Postinstall script review | Build scripts can run in CI with secrets | Use package manager settings and review new packages |
| Source map policy | Public source maps can expose internal code structure | Publish only intended source maps and restrict access |
| Environment variable review | Client-exposed variables can leak secrets | Allow only intentional NEXT_PUBLIC_ variables |
| Build artifact scanning | Secrets can enter bundles or .next output | Scan build artifacts before deployment |
| Third-party script inventory | Scripts can bypass app code controls | Require 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 behavior | Next.js headers, RSC requests, route patterns, build artifacts | Determines whether App Router, RSC, or Server Actions may be present |
| Enumerate public routes | Pages, route handlers, static assets, API-like paths | Finds unprotected entry points |
| Compare roles and tenants | Same route across different users | Detects BOLA, IDOR, cache leakage, and missing authorization |
| Inspect client bundles | Client-side secrets, route names, action references, source maps | Reveals hidden routes or leaked configuration |
| Test Server Actions as endpoints | Mutations with modified IDs and form fields | Finds missing action-level authorization |
| Test Proxy assumptions | Routes that should be protected but are not | Finds matcher drift and middleware-only auth |
| Review RSC payloads | Serialized props and component data | Finds sensitive data crossing the client boundary |
| Test Host and forwarded headers | Host, X-Forwarded-Host, Origin, scheme | Finds SSRF, redirect, CSRF, and proxy trust issues |
| Check cache isolation | Response reuse across users | Finds cross-user data exposure |
| Review dependency versions | Known CVEs and vulnerable framework packages | Finds 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.
| Area | Review question | Bad sign | Safer pattern |
|---|---|---|---|
| Server Components | Are raw database objects passed to Client Components? | return <Client user={user} /> with full ORM object | DTO with explicit public fields |
| Client boundary | Are server-only modules importable from client code? | لا يوجد server-only in data access modules | import "server-only" in server data modules |
| إجراءات الخادم | Does every action validate session and authorization internally? | Action trusts page-level auth | Action checks session, tenant, object ownership, permission |
| Proxy | Is Proxy the only auth layer? | Protected route depends only on matcher | Proxy for routing, app logic for authorization |
| RSC versions | Are React and Next.js versions patched for RSC advisories? | Unknown versions across apps | Central inventory and emergency patch lanes |
| Cache | Can private pages be cached publicly? | مفقود التحكم في ذاكرة التخزين المؤقت on account routes | private, no-store for user-specific responses |
| Rich HTML | Is untrusted HTML rendered directly? | Raw بشكل خطيرSetSetInnerHTML | Sanitizer, allowlist, tests, CSP |
| CSP | Is CSP absent or too permissive? | غير آمنة-مضمنة everywhere without review | nonce or hash policy where feasible |
| Headers | Are Host, Origin, and forwarded headers trusted blindly? | Redirects or URLs built from raw Host | allowlisted origins and trusted proxy config |
| Dependencies | Are framework CVEs handled manually and slowly? | No alerting or lockfile review | Dependabot, 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:
- Any Server Action that mutates state requires authorization review.
- Any Client Component receiving server data requires DTO review.
- Any route containing user-specific data requires cache review.
- Any use of
بشكل خطيرSetSetInnerHTMLrequires sanitizer and CSP review. - Any framework major upgrade requires security regression tests.
- Any critical React or Next.js advisory triggers inventory, patch, deploy, and validation.
- 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.
بشكل خطيرSetSetInnerHTMLbypasses 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.

