Critical severityCISA KEVNVD Advisory· Published Dec 3, 2025· Updated Feb 26, 2026
CVE-2025-55182
CVE-2025-55182
Description
A pre-authentication remote code execution vulnerability exists in React Server Components versions 19.0.0, 19.1.0, 19.1.1, and 19.2.0 including the following packages: react-server-dom-parcel, react-server-dom-turbopack, and react-server-dom-webpack. The vulnerable code unsafely deserializes payloads from HTTP requests to Server Function endpoints.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
react-server-dom-webpacknpm | >= 19.0.0, < 19.0.1 | 19.0.1 |
react-server-dom-webpacknpm | >= 19.1.0, < 19.1.2 | 19.1.2 |
react-server-dom-webpacknpm | >= 19.2.0, < 19.2.1 | 19.2.1 |
react-server-dom-turbopacknpm | >= 19.0.0, < 19.0.1 | 19.0.1 |
react-server-dom-turbopacknpm | >= 19.1.0, < 19.1.2 | 19.1.2 |
react-server-dom-turbopacknpm | >= 19.2.0, < 19.2.1 | 19.2.1 |
react-server-dom-parcelnpm | >= 19.0.0, < 19.0.1 | 19.0.1 |
react-server-dom-parcelnpm | >= 19.1.0, < 19.1.2 | 19.1.2 |
react-server-dom-parcelnpm | >= 19.2.0, < 19.2.1 | 19.2.1 |
Affected products
3- Meta/react-server-dom-parcelv5Range: 19.0.0
- Meta/react-server-dom-turbopackv5Range: 19.0.0
- Meta/react-server-dom-webpackv5Range: 19.0.0
Patches
17dc903cd29daPatch FlightReplyServer with fixes from ReactFlightClient (#35277)
9 files changed · +712 −278
packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js+23 −12 modified@@ -344,31 +344,42 @@ function decodeReplyFromBusboy<T>( // we queue any fields we receive until the previous file is done. queuedFields.push(name, value); } else { - resolveField(response, name, value); + try { + resolveField(response, name, value); + } catch (error) { + busboyStream.destroy(error); + } } }); busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { if (encoding.toLowerCase() === 'base64') { - throw new Error( - "React doesn't accept base64 encoded file uploads because we don't expect " + - "form data passed from a browser to ever encode data that way. If that's " + - 'the wrong assumption, we can easily fix it.', + busboyStream.destroy( + new Error( + "React doesn't accept base64 encoded file uploads because we don't expect " + + "form data passed from a browser to ever encode data that way. If that's " + + 'the wrong assumption, we can easily fix it.', + ), ); + return; } pendingFiles++; const file = resolveFileInfo(response, name, filename, mimeType); value.on('data', chunk => { resolveFileChunk(response, file, chunk); }); value.on('end', () => { - resolveFileComplete(response, name, file); - pendingFiles--; - if (pendingFiles === 0) { - // Release any queued fields - for (let i = 0; i < queuedFields.length; i += 2) { - resolveField(response, queuedFields[i], queuedFields[i + 1]); + try { + resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; } - queuedFields.length = 0; + } catch (error) { + busboyStream.destroy(error); } }); });
packages/react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel.js+6 −1 modified@@ -19,6 +19,8 @@ import { } from '../shared/ReactFlightImportMetadata'; import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; +import hasOwnProperty from 'shared/hasOwnProperty'; + export type ServerManifest = { [string]: Array<string>, }; @@ -78,7 +80,10 @@ export function preloadModule<T>( export function requireModule<T>(metadata: ClientReference<T>): T { const moduleExports = parcelRequire(metadata[ID]); - return moduleExports[metadata[NAME]]; + if (hasOwnProperty.call(moduleExports, metadata[NAME])) { + return moduleExports[metadata[NAME]]; + } + return (undefined: any); } export function getModuleDebugInfo<T>(
packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js+23 −12 modified@@ -572,31 +572,42 @@ export function decodeReplyFromBusboy<T>( // we queue any fields we receive until the previous file is done. queuedFields.push(name, value); } else { - resolveField(response, name, value); + try { + resolveField(response, name, value); + } catch (error) { + busboyStream.destroy(error); + } } }); busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { if (encoding.toLowerCase() === 'base64') { - throw new Error( - "React doesn't accept base64 encoded file uploads because we don't expect " + - "form data passed from a browser to ever encode data that way. If that's " + - 'the wrong assumption, we can easily fix it.', + busboyStream.destroy( + new Error( + "React doesn't accept base64 encoded file uploads because we don't expect " + + "form data passed from a browser to ever encode data that way. If that's " + + 'the wrong assumption, we can easily fix it.', + ), ); + return; } pendingFiles++; const file = resolveFileInfo(response, name, filename, mimeType); value.on('data', chunk => { resolveFileChunk(response, file, chunk); }); value.on('end', () => { - resolveFileComplete(response, name, file); - pendingFiles--; - if (pendingFiles === 0) { - // Release any queued fields - for (let i = 0; i < queuedFields.length; i += 2) { - resolveField(response, queuedFields[i], queuedFields[i + 1]); + try { + resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; } - queuedFields.length = 0; + } catch (error) { + busboyStream.destroy(error); } }); });
packages/react-server-dom-turbopack/src/client/ReactFlightClientConfigBundlerTurbopack.js+6 −1 modified@@ -34,6 +34,8 @@ import { addChunkDebugInfo, } from 'react-client/src/ReactFlightClientConfig'; +import hasOwnProperty from 'shared/hasOwnProperty'; + export type ServerConsumerModuleMap = null | { [clientId: string]: { [clientExportName: string]: ClientReferenceManifestEntry, @@ -245,7 +247,10 @@ export function requireModule<T>(metadata: ClientReference<T>): T { // default property of this if it was an ESM interop module. return moduleExports.__esModule ? moduleExports.default : moduleExports; } - return moduleExports[metadata[NAME]]; + if (hasOwnProperty.call(moduleExports, metadata[NAME])) { + return moduleExports[metadata[NAME]]; + } + return (undefined: any); } export function getModuleDebugInfo<T>(
packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js+23 −12 modified@@ -564,31 +564,42 @@ function decodeReplyFromBusboy<T>( // we queue any fields we receive until the previous file is done. queuedFields.push(name, value); } else { - resolveField(response, name, value); + try { + resolveField(response, name, value); + } catch (error) { + busboyStream.destroy(error); + } } }); busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { if (encoding.toLowerCase() === 'base64') { - throw new Error( - "React doesn't accept base64 encoded file uploads because we don't expect " + - "form data passed from a browser to ever encode data that way. If that's " + - 'the wrong assumption, we can easily fix it.', + busboyStream.destroy( + new Error( + "React doesn't accept base64 encoded file uploads because we don't expect " + + "form data passed from a browser to ever encode data that way. If that's " + + 'the wrong assumption, we can easily fix it.', + ), ); + return; } pendingFiles++; const file = resolveFileInfo(response, name, filename, mimeType); value.on('data', chunk => { resolveFileChunk(response, file, chunk); }); value.on('end', () => { - resolveFileComplete(response, name, file); - pendingFiles--; - if (pendingFiles === 0) { - // Release any queued fields - for (let i = 0; i < queuedFields.length; i += 2) { - resolveField(response, queuedFields[i], queuedFields[i + 1]); + try { + resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; } - queuedFields.length = 0; + } catch (error) { + busboyStream.destroy(error); } }); });
packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode.js+6 −1 modified@@ -24,6 +24,8 @@ import { } from '../shared/ReactFlightImportMetadata'; import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig'; +import hasOwnProperty from 'shared/hasOwnProperty'; + export type ServerConsumerModuleMap = { [clientId: string]: { [clientExportName: string]: ClientReference<any>, @@ -158,7 +160,10 @@ export function requireModule<T>(metadata: ClientReference<T>): T { // default property of this if it was an ESM interop module. return moduleExports.default; } - return moduleExports[metadata.name]; + if (hasOwnProperty.call(moduleExports, metadata.name)) { + return moduleExports[metadata.name]; + } + return (undefined: any); } export function getModuleDebugInfo<T>(metadata: ClientReference<T>): null {
packages/react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerWebpack.js+6 −1 modified@@ -34,6 +34,8 @@ import { addChunkDebugInfo, } from 'react-client/src/ReactFlightClientConfig'; +import hasOwnProperty from 'shared/hasOwnProperty'; + export type ServerConsumerModuleMap = null | { [clientId: string]: { [clientExportName: string]: ClientReferenceManifestEntry, @@ -253,7 +255,10 @@ export function requireModule<T>(metadata: ClientReference<T>): T { // default property of this if it was an ESM interop module. return moduleExports.__esModule ? moduleExports.default : moduleExports; } - return moduleExports[metadata[NAME]]; + if (hasOwnProperty.call(moduleExports, metadata[NAME])) { + return moduleExports[metadata[NAME]]; + } + return (undefined: any); } export function getModuleDebugInfo<T>(
packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js+23 −12 modified@@ -564,31 +564,42 @@ function decodeReplyFromBusboy<T>( // we queue any fields we receive until the previous file is done. queuedFields.push(name, value); } else { - resolveField(response, name, value); + try { + resolveField(response, name, value); + } catch (error) { + busboyStream.destroy(error); + } } }); busboyStream.on('file', (name, value, {filename, encoding, mimeType}) => { if (encoding.toLowerCase() === 'base64') { - throw new Error( - "React doesn't accept base64 encoded file uploads because we don't expect " + - "form data passed from a browser to ever encode data that way. If that's " + - 'the wrong assumption, we can easily fix it.', + busboyStream.destroy( + new Error( + "React doesn't accept base64 encoded file uploads because we don't expect " + + "form data passed from a browser to ever encode data that way. If that's " + + 'the wrong assumption, we can easily fix it.', + ), ); + return; } pendingFiles++; const file = resolveFileInfo(response, name, filename, mimeType); value.on('data', chunk => { resolveFileChunk(response, file, chunk); }); value.on('end', () => { - resolveFileComplete(response, name, file); - pendingFiles--; - if (pendingFiles === 0) { - // Release any queued fields - for (let i = 0; i < queuedFields.length; i += 2) { - resolveField(response, queuedFields[i], queuedFields[i + 1]); + try { + resolveFileComplete(response, name, file); + pendingFiles--; + if (pendingFiles === 0) { + // Release any queued fields + for (let i = 0; i < queuedFields.length; i += 2) { + resolveField(response, queuedFields[i], queuedFields[i + 1]); + } + queuedFields.length = 0; } - queuedFields.length = 0; + } catch (error) { + busboyStream.destroy(error); } }); });
packages/react-server/src/ReactFlightReplyServer.js+596 −226 modified@@ -50,44 +50,35 @@ export type JSONValue = const PENDING = 'pending'; const BLOCKED = 'blocked'; -const CYCLIC = 'cyclic'; const RESOLVED_MODEL = 'resolved_model'; const INITIALIZED = 'fulfilled'; const ERRORED = 'rejected'; +type RESPONSE_SYMBOL_TYPE = 'RESPONSE_SYMBOL'; // Fake symbol type. +const RESPONSE_SYMBOL: RESPONSE_SYMBOL_TYPE = (Symbol(): any); + type PendingChunk<T> = { status: 'pending', - value: null | Array<(T) => mixed>, - reason: null | Array<(mixed) => mixed>, - _response: Response, + value: null | Array<InitializationReference | (T => mixed)>, + reason: null | Array<InitializationReference | (mixed => mixed)>, then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type BlockedChunk<T> = { status: 'blocked', - value: null | Array<(T) => mixed>, - reason: null | Array<(mixed) => mixed>, - _response: Response, - then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, -}; -type CyclicChunk<T> = { - status: 'cyclic', - value: null | Array<(T) => mixed>, - reason: null | Array<(mixed) => mixed>, - _response: Response, + value: null | Array<InitializationReference | (T => mixed)>, + reason: null | Array<InitializationReference | (mixed => mixed)>, then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type ResolvedModelChunk<T> = { status: 'resolved_model', value: string, - reason: number, - _response: Response, + reason: {id: number, [RESPONSE_SYMBOL_TYPE]: Response}, then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type InitializedChunk<T> = { status: 'fulfilled', value: T, reason: null, - _response: Response, then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type InitializedStreamChunk< @@ -96,38 +87,34 @@ type InitializedStreamChunk< status: 'fulfilled', value: T, reason: FlightStreamController, - _response: Response, then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void, }; type ErroredChunk<T> = { status: 'rejected', value: null, reason: mixed, - _response: Response, then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type SomeChunk<T> = | PendingChunk<T> | BlockedChunk<T> - | CyclicChunk<T> | ResolvedModelChunk<T> | InitializedChunk<T> | ErroredChunk<T>; // $FlowFixMe[missing-this-annot] -function Chunk(status: any, value: any, reason: any, response: Response) { +function ReactPromise(status: any, value: any, reason: any) { this.status = status; this.value = value; this.reason = reason; - this._response = response; } // We subclass Promise.prototype so that we get other methods like .catch -Chunk.prototype = (Object.create(Promise.prototype): any); +ReactPromise.prototype = (Object.create(Promise.prototype): any); // TODO: This doesn't return a new Promise chain unlike the real .then -Chunk.prototype.then = function <T>( +ReactPromise.prototype.then = function <T>( this: SomeChunk<T>, resolve: (value: T) => mixed, - reject: (reason: mixed) => mixed, + reject: ?(reason: mixed) => mixed, ) { const chunk: SomeChunk<T> = this; // If we have resolved content, we try to initialize it first which @@ -140,26 +127,31 @@ Chunk.prototype.then = function <T>( // The status might have changed after initialization. switch (chunk.status) { case INITIALIZED: - resolve(chunk.value); + if (typeof resolve === 'function') { + resolve(chunk.value); + } break; case PENDING: case BLOCKED: - case CYCLIC: - if (resolve) { + if (typeof resolve === 'function') { if (chunk.value === null) { - chunk.value = ([]: Array<(T) => mixed>); + chunk.value = ([]: Array<InitializationReference | (T => mixed)>); } chunk.value.push(resolve); } - if (reject) { + if (typeof reject === 'function') { if (chunk.reason === null) { - chunk.reason = ([]: Array<(mixed) => mixed>); + chunk.reason = ([]: Array< + InitializationReference | (mixed => mixed), + >); } chunk.reason.push(reject); } break; default: - reject(chunk.reason); + if (typeof reject === 'function') { + reject(chunk.reason); + } break; } }; @@ -181,28 +173,114 @@ export function getRoot<T>(response: Response): Thenable<T> { function createPendingChunk<T>(response: Response): PendingChunk<T> { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(PENDING, null, null, response); + return new ReactPromise(PENDING, null, null); } -function wakeChunk<T>(listeners: Array<(T) => mixed>, value: T): void { +function wakeChunk<T>( + response: Response, + listeners: Array<InitializationReference | (T => mixed)>, + value: T, +): void { for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; - listener(value); + if (typeof listener === 'function') { + listener(value); + } else { + fulfillReference(response, listener, value); + } } } +function rejectChunk( + response: Response, + listeners: Array<InitializationReference | (mixed => mixed)>, + error: mixed, +): void { + for (let i = 0; i < listeners.length; i++) { + const listener = listeners[i]; + if (typeof listener === 'function') { + listener(error); + } else { + rejectReference(response, listener.handler, error); + } + } +} + +function resolveBlockedCycle<T>( + resolvedChunk: SomeChunk<T>, + reference: InitializationReference, +): null | InitializationHandler { + const referencedChunk = reference.handler.chunk; + if (referencedChunk === null) { + return null; + } + if (referencedChunk === resolvedChunk) { + // We found the cycle. We can resolve the blocked cycle now. + return reference.handler; + } + const resolveListeners = referencedChunk.value; + if (resolveListeners !== null) { + for (let i = 0; i < resolveListeners.length; i++) { + const listener = resolveListeners[i]; + if (typeof listener !== 'function') { + const foundHandler = resolveBlockedCycle(resolvedChunk, listener); + if (foundHandler !== null) { + return foundHandler; + } + } + } + } + return null; +} + function wakeChunkIfInitialized<T>( + response: Response, chunk: SomeChunk<T>, - resolveListeners: Array<(T) => mixed>, - rejectListeners: null | Array<(mixed) => mixed>, + resolveListeners: Array<InitializationReference | (T => mixed)>, + rejectListeners: null | Array<InitializationReference | (mixed => mixed)>, ): void { switch (chunk.status) { case INITIALIZED: - wakeChunk(resolveListeners, chunk.value); + wakeChunk(response, resolveListeners, chunk.value); break; - case PENDING: case BLOCKED: - case CYCLIC: + // It is possible that we're blocked on our own chunk if it's a cycle. + // Before adding back the listeners to the chunk, let's check if it would + // result in a cycle. + for (let i = 0; i < resolveListeners.length; i++) { + const listener = resolveListeners[i]; + if (typeof listener !== 'function') { + const reference: InitializationReference = listener; + const cyclicHandler = resolveBlockedCycle(chunk, reference); + if (cyclicHandler !== null) { + // This reference points back to this chunk. We can resolve the cycle by + // using the value from that handler. + fulfillReference(response, reference, cyclicHandler.value); + resolveListeners.splice(i, 1); + i--; + if (rejectListeners !== null) { + const rejectionIdx = rejectListeners.indexOf(reference); + if (rejectionIdx !== -1) { + rejectListeners.splice(rejectionIdx, 1); + } + } + // The status might have changed after fulfilling the reference. + switch ((chunk: SomeChunk<T>).status) { + case INITIALIZED: + const initializedChunk: InitializedChunk<T> = (chunk: any); + wakeChunk(response, resolveListeners, initializedChunk.value); + return; + case ERRORED: + if (rejectListeners !== null) { + rejectChunk(response, rejectListeners, chunk.reason); + } + return; + } + } + } + } + // Fallthrough + case PENDING: if (chunk.value) { for (let i = 0; i < resolveListeners.length; i++) { chunk.value.push(resolveListeners[i]); @@ -223,13 +301,17 @@ function wakeChunkIfInitialized<T>( break; case ERRORED: if (rejectListeners) { - wakeChunk(rejectListeners, chunk.reason); + wakeChunk(response, rejectListeners, chunk.reason); } break; } } -function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void { +function triggerErrorOnChunk<T>( + response: Response, + chunk: SomeChunk<T>, + error: mixed, +): void { if (chunk.status !== PENDING && chunk.status !== BLOCKED) { // If we get more data to an already resolved ID, we assume that it's // a stream chunk since any other row shouldn't have more than one entry. @@ -244,7 +326,7 @@ function triggerErrorOnChunk<T>(chunk: SomeChunk<T>, error: mixed): void { erroredChunk.status = ERRORED; erroredChunk.reason = error; if (listeners !== null) { - wakeChunk(listeners, error); + rejectChunk(response, listeners, error); } } @@ -254,18 +336,22 @@ function createResolvedModelChunk<T>( id: number, ): ResolvedModelChunk<T> { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(RESOLVED_MODEL, value, id, response); + return new ReactPromise(RESOLVED_MODEL, value, { + id, + [RESPONSE_SYMBOL]: response, + }); } function createErroredChunk<T>( response: Response, reason: mixed, ): ErroredChunk<T> { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(ERRORED, null, reason, response); + return new ReactPromise(ERRORED, null, reason); } function resolveModelChunk<T>( + response: Response, chunk: SomeChunk<T>, value: string, id: number, @@ -287,14 +373,14 @@ function resolveModelChunk<T>( const resolvedChunk: ResolvedModelChunk<T> = (chunk: any); resolvedChunk.status = RESOLVED_MODEL; resolvedChunk.value = value; - resolvedChunk.reason = id; + resolvedChunk.reason = {id, [RESPONSE_SYMBOL]: response}; if (resolveListeners !== null) { // This is unfortunate that we're reading this eagerly if // we already have listeners attached since they might no // longer be rendered or might not be the highest pri. initializeModelChunk(resolvedChunk); // The status might have changed after initialization. - wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners); + wakeChunkIfInitialized(response, chunk, resolveListeners, rejectListeners); } } @@ -308,7 +394,7 @@ function createInitializedStreamChunk< // We use the reason field to stash the controller since we already have that // field. It's a bit of a hack but efficient. // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(INITIALIZED, value, controller, response); + return new ReactPromise(INITIALIZED, value, controller); } function createResolvedIteratorResultChunk<T>( @@ -320,66 +406,127 @@ function createResolvedIteratorResultChunk<T>( const iteratorResultJSON = (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}'; // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk(RESOLVED_MODEL, iteratorResultJSON, -1, response); + return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, { + id: -1, + [RESPONSE_SYMBOL]: response, + }); } function resolveIteratorResultChunk<T>( + response: Response, chunk: SomeChunk<IteratorResult<T, T>>, value: string, done: boolean, ): void { // To reuse code as much code as possible we add the wrapper element as part of the JSON. const iteratorResultJSON = (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}'; - resolveModelChunk(chunk, iteratorResultJSON, -1); -} - -function bindArgs(fn: any, args: any) { - return fn.bind.apply(fn, [null].concat(args)); + resolveModelChunk(response, chunk, iteratorResultJSON, -1); } -function loadServerReference<T>( +function loadServerReference<A: Iterable<any>, T>( response: Response, - id: ServerReferenceId, - bound: null | Thenable<Array<any>>, - parentChunk: SomeChunk<T>, + metaData: { + id: any, + bound: null | Thenable<Array<any>>, + }, parentObject: Object, key: string, -): T { +): (...A) => Promise<T> { + const id: ServerReferenceId = metaData.id; + if (typeof id !== 'string') { + return (null: any); + } const serverReference: ServerReference<T> = resolveServerReference<$FlowFixMe>(response._bundlerConfig, id); // We expect most servers to not really need this because you'd just have all // the relevant modules already loaded but it allows for lazy loading of code // if needed. - const preloadPromise = preloadModule(serverReference); - let promise: Promise<T>; - if (bound) { - promise = Promise.all([(bound: any), preloadPromise]).then( - ([args]: Array<any>) => bindArgs(requireModule(serverReference), args), - ); - } else { - if (preloadPromise) { - promise = Promise.resolve(preloadPromise).then(() => - requireModule(serverReference), - ); + const bound = metaData.bound; + let promise: null | Thenable<any> = preloadModule(serverReference); + if (!promise) { + if (bound instanceof ReactPromise) { + promise = Promise.resolve(bound); } else { - // Synchronously available - return requireModule(serverReference); + const resolvedValue = (requireModule(serverReference): any); + return resolvedValue; } + } else if (bound instanceof ReactPromise) { + promise = Promise.all([promise, bound]); } - promise.then( - createModelResolver( - parentChunk, - parentObject, - key, - false, - response, - createModel, - [], - ), - createModelReject(parentChunk), - ); - // We need a placeholder value that will be replaced later. + + let handler: InitializationHandler; + if (initializingHandler) { + handler = initializingHandler; + handler.deps++; + } else { + handler = initializingHandler = { + chunk: null, + value: null, + reason: null, + deps: 1, + errored: false, + }; + } + + function fulfill(): void { + let resolvedValue = (requireModule(serverReference): any); + + if (metaData.bound) { + // This promise is coming from us and should have initilialized by now. + const promiseValue = (metaData.bound: any).value; + const boundArgs: Array<any> = Array.isArray(promiseValue) + ? promiseValue.slice(0) + : []; + boundArgs.unshift(null); // this + resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs); + } + + parentObject[key] = resolvedValue; + + // If this is the root object for a model reference, where `handler.value` + // is a stale `null`, the resolved value can be used directly. + if (key === '' && handler.value === null) { + handler.value = resolvedValue; + } + + handler.deps--; + + if (handler.deps === 0) { + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + const resolveListeners = chunk.value; + const initializedChunk: InitializedChunk<T> = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = handler.value; + if (resolveListeners !== null) { + wakeChunk(response, resolveListeners, handler.value); + } + } + } + + function reject(error: mixed): void { + if (handler.errored) { + // We've already errored. We could instead build up an AggregateError + // but if there are multiple errors we just take the first one like + // Promise.all. + return; + } + handler.errored = true; + handler.value = null; + handler.reason = error; + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + triggerErrorOnChunk(response, chunk, error); + } + + promise.then(fulfill, reject); + + // Return a place holder value for now. return (null: any); } @@ -427,7 +574,7 @@ function reviveModel( value[key], childRef, ); - if (newValue !== undefined) { + if (newValue !== undefined || key === '__proto__') { // $FlowFixMe[cannot-write] value[key] = newValue; } else { @@ -441,62 +588,93 @@ function reviveModel( return value; } -let initializingChunk: ResolvedModelChunk<any> = (null: any); -let initializingChunkBlockedModel: null | {deps: number, value: any} = null; +type InitializationReference = { + handler: InitializationHandler, + parentObject: Object, + key: string, + map: ( + response: Response, + model: any, + parentObject: Object, + key: string, + ) => any, + path: Array<string>, +}; +type InitializationHandler = { + chunk: null | BlockedChunk<any>, + value: any, + reason: any, + deps: number, + errored: boolean, +}; +let initializingHandler: null | InitializationHandler = null; + function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void { - const prevChunk = initializingChunk; - const prevBlocked = initializingChunkBlockedModel; - initializingChunk = chunk; - initializingChunkBlockedModel = null; + const prevHandler = initializingHandler; + initializingHandler = null; - const rootReference = - chunk.reason === -1 ? undefined : chunk.reason.toString(16); + const {[RESPONSE_SYMBOL]: response, id} = chunk.reason; + + const rootReference = id === -1 ? undefined : id.toString(16); const resolvedModel = chunk.value; - // We go to the CYCLIC state until we've fully resolved this. + // We go to the BLOCKED state until we've fully resolved this. // We do this before parsing in case we try to initialize the same chunk // while parsing the model. Such as in a cyclic reference. - const cyclicChunk: CyclicChunk<T> = (chunk: any); - cyclicChunk.status = CYCLIC; + const cyclicChunk: BlockedChunk<T> = (chunk: any); + cyclicChunk.status = BLOCKED; cyclicChunk.value = null; cyclicChunk.reason = null; try { const rawModel = JSON.parse(resolvedModel); const value: T = reviveModel( - chunk._response, + response, {'': rawModel}, '', rawModel, rootReference, ); - if ( - initializingChunkBlockedModel !== null && - initializingChunkBlockedModel.deps > 0 - ) { - initializingChunkBlockedModel.value = value; - // We discovered new dependencies on modules that are not yet resolved. - // We have to go the BLOCKED state until they're resolved. - const blockedChunk: BlockedChunk<T> = (chunk: any); - blockedChunk.status = BLOCKED; - } else { - const resolveListeners = cyclicChunk.value; - const initializedChunk: InitializedChunk<T> = (chunk: any); - initializedChunk.status = INITIALIZED; - initializedChunk.value = value; - if (resolveListeners !== null) { - wakeChunk(resolveListeners, value); + + // Invoke any listeners added while resolving this model. I.e. cyclic + // references. This may or may not fully resolve the model depending on + // if they were blocked. + const resolveListeners = cyclicChunk.value; + if (resolveListeners !== null) { + cyclicChunk.value = null; + cyclicChunk.reason = null; + for (let i = 0; i < resolveListeners.length; i++) { + const listener = resolveListeners[i]; + if (typeof listener === 'function') { + listener(value); + } else { + fulfillReference(response, listener, value); + } } } + if (initializingHandler !== null) { + if (initializingHandler.errored) { + throw initializingHandler.reason; + } + if (initializingHandler.deps > 0) { + // We discovered new dependencies on modules that are not yet resolved. + // We have to keep the BLOCKED state until they're resolved. + initializingHandler.value = value; + initializingHandler.chunk = cyclicChunk; + return; + } + } + const initializedChunk: InitializedChunk<T> = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = value; } catch (error) { const erroredChunk: ErroredChunk<T> = (chunk: any); erroredChunk.status = ERRORED; erroredChunk.reason = error; } finally { - initializingChunk = prevChunk; - initializingChunkBlockedModel = prevBlocked; + initializingHandler = prevHandler; } } @@ -510,7 +688,7 @@ export function reportGlobalError(response: Response, error: Error): void { // trigger an error but if it wasn't then we need to // because we won't be getting any new data to resolve it. if (chunk.status === PENDING) { - triggerErrorOnChunk(chunk, error); + triggerErrorOnChunk(response, chunk, error); } }); } @@ -523,9 +701,8 @@ function getChunk(response: Response, id: number): SomeChunk<any> { const key = prefix + id; // Check if we have this field in the backing store already. const backingEntry = response._formData.get(key); - if (backingEntry != null) { - // We assume that this is a string entry for now. - chunk = createResolvedModelChunk(response, (backingEntry: any), id); + if (typeof backingEntry === 'string') { + chunk = createResolvedModelChunk(response, backingEntry, id); } else if (response._closed) { // We have already errored the response and we're not going to get // anything more streaming in so this will immediately error. @@ -539,65 +716,160 @@ function getChunk(response: Response, id: number): SomeChunk<any> { return chunk; } -function createModelResolver<T>( - chunk: SomeChunk<T>, +function fulfillReference( + response: Response, + reference: InitializationReference, + value: any, +): void { + const {handler, parentObject, key, map, path} = reference; + + for (let i = 1; i < path.length; i++) { + // The server doesn't have any lazy references but we unwrap Chunks here in the same way as the client. + while (value instanceof ReactPromise) { + const referencedChunk: SomeChunk<any> = value; + switch (referencedChunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(referencedChunk); + break; + } + switch (referencedChunk.status) { + case INITIALIZED: { + value = referencedChunk.value; + continue; + } + case BLOCKED: + case PENDING: { + // If we're not yet initialized we need to skip what we've already drilled + // through and then wait for the next value to become available. + path.splice(0, i - 1); + // Add "listener" to our new chunk dependency. + if (referencedChunk.value === null) { + referencedChunk.value = [reference]; + } else { + referencedChunk.value.push(reference); + } + if (referencedChunk.reason === null) { + referencedChunk.reason = [reference]; + } else { + referencedChunk.reason.push(reference); + } + return; + } + default: { + rejectReference(response, reference.handler, referencedChunk.reason); + return; + } + } + } + const name = path[i]; + if (typeof value === 'object' && hasOwnProperty.call(value, name)) { + value = value[name]; + } + } + + const mappedValue = map(response, value, parentObject, key); + parentObject[key] = mappedValue; + + // If this is the root object for a model reference, where `handler.value` + // is a stale `null`, the resolved value can be used directly. + if (key === '' && handler.value === null) { + handler.value = mappedValue; + } + + // There are no Elements or Debug Info to transfer here. + + handler.deps--; + + if (handler.deps === 0) { + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + const resolveListeners = chunk.value; + const initializedChunk: InitializedChunk<any> = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = handler.value; + initializedChunk.reason = handler.reason; // Used by streaming chunks + if (resolveListeners !== null) { + wakeChunk(response, resolveListeners, handler.value); + } + } +} + +function rejectReference( + response: Response, + handler: InitializationHandler, + error: mixed, +): void { + if (handler.errored) { + // We've already errored. We could instead build up an AggregateError + // but if there are multiple errors we just take the first one like + // Promise.all. + return; + } + handler.errored = true; + handler.value = null; + handler.reason = error; + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + // There's no debug info to forward in this direction. + triggerErrorOnChunk(response, chunk, error); +} + +function waitForReference<T>( + referencedChunk: PendingChunk<T> | BlockedChunk<T>, parentObject: Object, key: string, - cyclic: boolean, response: Response, - map: (response: Response, model: any) => T, + map: (response: Response, model: any, parentObject: Object, key: string) => T, path: Array<string>, -): (value: any) => void { - let blocked; - if (initializingChunkBlockedModel) { - blocked = initializingChunkBlockedModel; - if (!cyclic) { - blocked.deps++; - } +): T { + let handler: InitializationHandler; + if (initializingHandler) { + handler = initializingHandler; + handler.deps++; } else { - blocked = initializingChunkBlockedModel = { - deps: (cyclic ? 0 : 1) as number, - value: (null: any), + handler = initializingHandler = { + chunk: null, + value: null, + reason: null, + deps: 1, + errored: false, }; } - return value => { - for (let i = 1; i < path.length; i++) { - value = value[path[i]]; - } - parentObject[key] = map(response, value); - // If this is the root object for a model reference, where `blocked.value` - // is a stale `null`, the resolved value can be used directly. - if (key === '' && blocked.value === null) { - blocked.value = parentObject[key]; - } - - blocked.deps--; - if (blocked.deps === 0) { - if (chunk.status !== BLOCKED) { - return; - } - const resolveListeners = chunk.value; - const initializedChunk: InitializedChunk<T> = (chunk: any); - initializedChunk.status = INITIALIZED; - initializedChunk.value = blocked.value; - if (resolveListeners !== null) { - wakeChunk(resolveListeners, blocked.value); - } - } + const reference: InitializationReference = { + handler, + parentObject, + key, + map, + path, }; -} -function createModelReject<T>(chunk: SomeChunk<T>): (error: mixed) => void { - return (error: mixed) => triggerErrorOnChunk(chunk, error); + // Add "listener". + if (referencedChunk.value === null) { + referencedChunk.value = [reference]; + } else { + referencedChunk.value.push(reference); + } + if (referencedChunk.reason === null) { + referencedChunk.reason = [reference]; + } else { + referencedChunk.reason.push(reference); + } + + // Return a place holder value for now. + return (null: any); } function getOutlinedModel<T>( response: Response, reference: string, parentObject: Object, key: string, - map: (response: Response, model: any) => T, + map: (response: Response, model: any, parentObject: Object, key: string) => T, ): T { const path = reference.split(':'); const id = parseInt(path[0], 16); @@ -612,28 +884,79 @@ function getOutlinedModel<T>( case INITIALIZED: let value = chunk.value; for (let i = 1; i < path.length; i++) { - value = value[path[i]]; + // The server doesn't have any lazy references but we unwrap Chunks here in the same way as the client. + while (value instanceof ReactPromise) { + const referencedChunk: SomeChunk<any> = value; + switch (referencedChunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(referencedChunk); + break; + } + switch (referencedChunk.status) { + case INITIALIZED: { + value = referencedChunk.value; + break; + } + case BLOCKED: + case PENDING: { + return waitForReference( + referencedChunk, + parentObject, + key, + response, + map, + path.slice(i - 1), + ); + } + default: { + // This is an error. Instead of erroring directly, we're going to encode this on + // an initialization handler so that we can catch it at the nearest Element. + if (initializingHandler) { + initializingHandler.errored = true; + initializingHandler.value = null; + initializingHandler.reason = referencedChunk.reason; + } else { + initializingHandler = { + chunk: null, + value: null, + reason: referencedChunk.reason, + deps: 0, + errored: true, + }; + } + return (null: any); + } + } + } + const name = path[i]; + if (typeof value === 'object' && hasOwnProperty.call(value, name)) { + value = value[name]; + } } - return map(response, value); + const chunkValue = map(response, value, parentObject, key); + // There's no Element nor Debug Info in the ReplyServer so we don't have to check those here. + return chunkValue; case PENDING: case BLOCKED: - case CYCLIC: - const parentChunk = initializingChunk; - chunk.then( - createModelResolver( - parentChunk, - parentObject, - key, - chunk.status === CYCLIC, - response, - map, - path, - ), - createModelReject(parentChunk), - ); - return (null: any); + return waitForReference(chunk, parentObject, key, response, map, path); default: - throw chunk.reason; + // This is an error. Instead of erroring directly, we're going to encode this on + // an initialization handler. + if (initializingHandler) { + initializingHandler.errored = true; + initializingHandler.value = null; + initializingHandler.reason = chunk.reason; + } else { + initializingHandler = { + chunk: null, + value: null, + reason: chunk.reason, + deps: 0, + errored: true, + }; + } + // Placeholder + return (null: any); } } @@ -657,7 +980,7 @@ function createModel(response: Response, model: any): any { return model; } -function parseTypedArray( +function parseTypedArray<T: $ArrayBufferView | ArrayBuffer>( response: Response, reference: string, constructor: any, @@ -670,30 +993,78 @@ function parseTypedArray( const key = prefix + id; // We should have this backingEntry in the store already because we emitted // it before referencing it. It should be a Blob. + // TODO: Use getOutlinedModel to allow us to emit the Blob later. We should be able to do that now. const backingEntry: Blob = (response._formData.get(key): any); - const promise = - constructor === ArrayBuffer - ? backingEntry.arrayBuffer() - : backingEntry.arrayBuffer().then(function (buffer) { - return new constructor(buffer); - }); + const promise: Promise<ArrayBuffer> = backingEntry.arrayBuffer(); // Since loading the buffer is an async operation we'll be blocking the parent // chunk. - const parentChunk = initializingChunk; - promise.then( - createModelResolver( - parentChunk, - parentObject, - parentKey, - false, - response, - createModel, - [], - ), - createModelReject(parentChunk), - ); + + let handler: InitializationHandler; + if (initializingHandler) { + handler = initializingHandler; + handler.deps++; + } else { + handler = initializingHandler = { + chunk: null, + value: null, + reason: null, + deps: 1, + errored: false, + }; + } + + function fulfill(buffer: ArrayBuffer): void { + const resolvedValue: T = + constructor === ArrayBuffer + ? (buffer: any) + : (new constructor(buffer): any); + + parentObject[parentKey] = resolvedValue; + + // If this is the root object for a model reference, where `handler.value` + // is a stale `null`, the resolved value can be used directly. + if (parentKey === '' && handler.value === null) { + handler.value = resolvedValue; + } + + handler.deps--; + + if (handler.deps === 0) { + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + const resolveListeners = chunk.value; + const initializedChunk: InitializedChunk<T> = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = handler.value; + if (resolveListeners !== null) { + wakeChunk(response, resolveListeners, handler.value); + } + } + } + + function reject(error: mixed): void { + if (handler.errored) { + // We've already errored. We could instead build up an AggregateError + // but if there are multiple errors we just take the first one like + // Promise.all. + return; + } + handler.errored = true; + handler.value = null; + handler.reason = error; + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + triggerErrorOnChunk(response, chunk, error); + } + + promise.then(fulfill, reject); + return null; } @@ -711,12 +1082,13 @@ function resolveStream<T: ReadableStream | $AsyncIterable<any, any, void>>( const key = prefix + id; const existingEntries = response._formData.getAll(key); for (let i = 0; i < existingEntries.length; i++) { - // We assume that this is a string entry for now. - const value: string = (existingEntries[i]: any); - if (value[0] === 'C') { - controller.close(value === 'C' ? '"$undefined"' : value.slice(1)); - } else { - controller.enqueueModel(value); + const value = existingEntries[i]; + if (typeof value === 'string') { + if (value[0] === 'C') { + controller.close(value === 'C' ? '"$undefined"' : value.slice(1)); + } else { + controller.enqueueModel(value); + } } } } @@ -774,7 +1146,7 @@ function parseReadableStream<T>( // to synchronous emitting. previousBlockedChunk = null; } - resolveModelChunk(chunk, json, -1); + resolveModelChunk(response, chunk, json, -1); }); } }, @@ -844,7 +1216,12 @@ function parseAsyncIterable<T>( false, ); } else { - resolveIteratorResultChunk(buffer[nextWriteIndex], value, false); + resolveIteratorResultChunk( + response, + buffer[nextWriteIndex], + value, + false, + ); } nextWriteIndex++; }, @@ -857,12 +1234,18 @@ function parseAsyncIterable<T>( true, ); } else { - resolveIteratorResultChunk(buffer[nextWriteIndex], value, true); + resolveIteratorResultChunk( + response, + buffer[nextWriteIndex], + value, + true, + ); } nextWriteIndex++; while (nextWriteIndex < buffer.length) { // In generators, any extra reads from the iterator have the value undefined. resolveIteratorResultChunk( + response, buffer[nextWriteIndex++], '"$undefined"', true, @@ -876,7 +1259,7 @@ function parseAsyncIterable<T>( createPendingChunk<IteratorResult<T, T>>(response); } while (nextWriteIndex < buffer.length) { - triggerErrorOnChunk(buffer[nextWriteIndex++], error); + triggerErrorOnChunk(response, buffer[nextWriteIndex++], error); } }, }; @@ -892,11 +1275,10 @@ function parseAsyncIterable<T>( if (nextReadIndex === buffer.length) { if (closed) { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new Chunk( + return new ReactPromise( INITIALIZED, {done: true, value: undefined}, null, - response, ); } buffer[nextReadIndex] = @@ -935,19 +1317,7 @@ function parseModelString( case 'F': { // Server Reference const ref = value.slice(2); - // TODO: Just encode this in the reference inline instead of as a model. - const metaData: { - id: ServerReferenceId, - bound: null | Thenable<Array<any>>, - } = getOutlinedModel(response, ref, obj, key, createModel); - return loadServerReference( - response, - metaData.id, - metaData.bound, - initializingChunk, - obj, - key, - ); + return getOutlinedModel(response, ref, obj, key, loadServerReference); } case 'T': { // Temporary Reference @@ -1121,7 +1491,7 @@ export function resolveField( const chunk = chunks.get(id); if (chunk) { // We were waiting on this key so now we can resolve it. - resolveModelChunk(chunk, value, id); + resolveModelChunk(response, chunk, value, id); } } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
12- github.com/advisories/GHSA-fv66-9v8q-g76rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-55182ghsaADVISORY
- www.openwall.com/lists/oss-security/2025/12/03/4ghsaWEB
- github.com/facebook/react/commit/7dc903cd29dac55efb4424853fd0442fef3a8700ghsaWEB
- github.com/facebook/react/pull/35277ghsaWEB
- github.com/facebook/react/releases/tag/v19.0.1ghsaWEB
- github.com/facebook/react/releases/tag/v19.1.2ghsaWEB
- github.com/facebook/react/releases/tag/v19.2.1ghsaWEB
- github.com/facebook/react/security/advisories/GHSA-fv66-9v8q-g76rghsaWEB
- news.ycombinator.com/itemghsaWEB
- react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-componentsghsax_refsource_CONFIRMWEB
- www.facebook.com/security/advisories/cve-2025-55182ghsax_refsource_CONFIRMWEB
News mentions
5- ‘PCPJack’ Worm Removes TeamPCP Infections, Steals CredentialsSecurityWeek · May 8, 2026
- New PCPJack worm steals credentials, cleans TeamPCP infectionsBleepingComputer · May 7, 2026
- PCPJack Credential Stealer Exploits 5 CVEs to Spread Worm-Like Across Cloud SystemsThe Hacker News · May 7, 2026
- What type of 'C2 on a sleep cycle' do they leave behind? Novel Chinese spy group found in critical networks in Poland, AsiaThe Register Security · Apr 30, 2026
- 27th April – Threat Intelligence ReportCheck Point Research · Apr 27, 2026