On December 3, 2025, the React core team disclosed CVE-2025-55182 — a critical, CVSS 10.0 remote code execution vulnerability in React Server Components caused by insecure deserialization of malformed HTTP payloads in the React Flight protocol. It affected React 19.0 through 19.2.0, was fixed in 19.0.1, 19.1.2, and 19.2.1, and was confirmed exploited in the wild, with Unit42 and Tenable both publishing writeups under the informal name "React2Shell." A near-duplicate, CVE-2025-66478, was filed against the Next.js integration and later rejected as a duplicate of the same root cause. A week later, on December 11, React disclosed two follow-ons from the same audit: CVE-2025-55183, a source-code exposure bug (CVSS 5.3) where a crafted request could trick a Server Action into returning its own source, and CVE-2025-55184/CVE-2025-67779, a denial-of-service flaw (CVSS 7.5) from malformed deserialization triggering infinite loops. All three point at the same architectural surface: the serialized payload that carries data from server to client. This post covers the CVEs, the older and more common mistake underneath them — conditional rendering is not access control — and where classic XSS sinks still bite.
What did CVE-2025-55182 actually break?
CVE-2025-55182 broke the trust boundary React draws around the Flight protocol, the wire format RSC uses to serialize server-rendered trees and pass them to the client runtime. The React team's December 3, 2025 advisory (react.dev/blog/2025/12/03) describes the root cause as insecure deserialization: a malformed HTTP request could be crafted so that attacker-controlled bytes influenced server-side execution during payload parsing, rather than being treated as inert data. That's what pushed the CVSS score to a maximum 10.0 — it wasn't a data leak, it was remote code execution on the rendering server. Because Server Components run exclusively server-side and stream their output as Flight payloads, any application on React 19.0–19.2.0 rendering RSC trees from network-facing requests was exposed until it upgraded to 19.0.1, 19.1.2, or 19.2.1. Next.js shipped corresponding point releases the same week, since the App Router's server rendering path is built directly on Flight.
Why doesn't {isAdmin && <AdminPanel data={secret} />} actually hide anything?
It doesn't hide anything from a determined viewer because RSC's serialization step runs before your conditional check has any bearing on what crosses the network. When a Server Component renders, React walks the whole tree and serializes every prop passed to every child into the Flight payload sent to the client — the client then hydrates only what the condition allows into the DOM. If secret is passed as a prop to AdminPanel and the surrounding JSX is gated with {isAdmin && <AdminPanel data={secret} />}, the danger depends entirely on where isAdmin is evaluated. Evaluate it in a Client Component and secret was already serialized server-side and is sitting in the Flight response your browser downloaded — visible in the Network tab regardless of whether the UI ever renders it. This is not a bug in React; it's a category error in how the check was placed. The fix is to make the server decide whether secret is ever fetched or passed at all, not to make the client decide whether to display data it already has.
Where does the fix for conditional-rendering leaks actually belong?
The fix belongs in the data-fetching layer on the server, not in the JSX that decides what to paint. A Server Component should check authorization before it queries or receives sensitive data — if (!isAdmin) return <Unauthorized /> before the database call, not after it — so that unauthorized branches of the tree never construct props containing the secret in the first place. Client Components should never receive a prop they aren't allowed to display; if a field is sensitive, omit it from the object returned to the client rather than passing the full record and filtering in JSX. Next.js's App Router documentation and React's own Server Components docs both frame this the same way: Server Components are for keeping data and logic off the client, but only if you actually stop the data at the server boundary. A select-style projection — returning only the fields a given user is authorized to see — is the reliable pattern; a boolean flag guarding a render is not.
Is dangerouslySetInnerHTML still the main XSS sink to worry about?
Yes — it remains React's officially named unsafe escape hatch, and it's still the sink most static analysis tools flag first for stored and reflected XSS. React's own documentation calls the prop "dangerous" specifically because it bypasses React's automatic escaping of interpolated values, injecting a raw HTML string directly into the DOM. Conditionally rendering it doesn't change the risk calculus: {showBio && <div dangerouslySetInnerHTML={{ __html: bio }} />} is exploitable exactly when bio originates from user input (a profile field, a comment, a CMS entry) and reaches this sink without sanitization — whether or not showBio is true for most users. A SAST engine tracing source-to-sink dataflow treats this the same way it treats innerHTML assignment or eval(): the conditional wrapper is irrelevant to whether the vulnerable path exists in the code, only to how often it executes at runtime. If you must render user-supplied HTML, run it through a sanitizer (DOMPurify is the common choice) immediately before the prop, not "when convenient."
What should a Next.js team actually change this week?
Three concrete changes cover most of the surface above. First, confirm your React and Next.js versions are past 19.0.1/19.1.2/19.2.1 (or the equivalent Next.js patch releases from the same week) — CVE-2025-55182 was exploited in the wild, so version currency here isn't optional hygiene, it's incident response. Second, audit Server Components that pass props conditionally to Client Components and move the authorization check to before data fetching, not around the JSX — grep for patterns like && < following a role or permission check and verify the data itself, not just the element, is gated. Third, treat every dangerouslySetInnerHTML call as a sink requiring a traced, sanitized source, the same way you'd treat a raw SQL string concatenation. None of these require a framework migration — they're a version bump and a data-flow review.
How Safeguard helps
Safeguard's SAST engine, part of its first-party application security testing, traces untrusted-input source-to-sink dataflows across JavaScript and TypeScript code, mapping each finding to its CWE and OWASP category with the full trace attached — the same technique that catches a dangerouslySetInnerHTML sink fed by unsanitized user input regardless of what conditional wraps it. Because findings carry a dataflow trace rather than a bare line number, a team can see exactly which branch of a Server Component actually reaches a sensitive prop or an HTML sink, instead of manually re-reading JSX to work out whether a guard is cosmetic or real. That reachability context is also what separates an urgent CVE-2025-55182-class dependency finding — a vulnerable React version your app actually renders through — from a theoretical one sitting in a lockfile nobody executes.