Apply React 19.0.7 RSC reply-decode DoS fixes (CVE-2026-23869, CVE-2026-23870) to vendored runtime#86
Conversation
The vendored react-server-dom-webpack builds were at the 19.0.4 security level, which predates two published React Server Components DoS advisories affecting decodeReply/decodeAction: - CVE-2026-23869 (GHSA-479c-33wc-g2pg), patched upstream in 19.0.5 - CVE-2026-23870 (GHSA-rv78-f8rc-xrxh), patched upstream in 19.0.6 Rather than rebasing the abanoubghadban/react fork patches (the runtime is slated for replacement by stock npm in #60), this applies the full npm 19.0.4 -> 19.0.7 built-artifact diff to the vendored cjs files, mirroring how dc4dfb4/867634f patched built artifacts in place. The 19.0.x line is security-only, so the diff is exactly the reply-decode hardening (FormData access wrapper, form-field prefix separator, initialized-stream-chunk guard, $$consumed ordering) plus version strings. All pre-existing in-repo deltas (Flight CSS hint emission from dc4dfb4/ 867634f/8781f08 and the fork's bound-args and rendererPackageName changes) are preserved byte-for-byte: the diff of each vendored file against stock 19.0.7 now equals the previous diff against stock 19.0.4. Verified: yarn build and yarn test (17 suites / 158 tests) pass, including NODE_CONDITIONS=react-server jest tests/react-flight-client-reference-css.rsc.test.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 17 minutes. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (18)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code Review: React 19.0.7 CVE Security PatchThis PR correctly applies upstream React 19.0.7 security patches to the vendored All 8 CJS build variants (browser/edge/node/unbundled × dev/prod) are updated consistently. The one hand-applied hunk (DevTools Security Analysis The three core hardening changes are correctly applied across all variants:
Wire-Format Interoperability Risk (Medium) The FormData prefix change is a breaking wire-format change between client (encodes) and server (decodes). A client on 19.0.4 sending a Before this ships: confirm that client-side and server-side React versions will update atomically in deployments. CDN-cached 19.0.4 JS bundles hitting a freshly deployed 19.0.7 server will silently mishandle Server Actions with nested FormData arguments until the cache expires.
The Test Coverage Gap The PR test plan verifies CSS Flight-hint behavior, build cleanliness, and baseline test suite. No tests exercise the specific patched paths:
These are exactly the paths the CVE hardening touches. This is inherent to testing vendored compiled artifacts, but it is the primary risk of the stopgap approach. Completing #60 would bring these paths under the upstream React test suite. Minor Notes
Verdict Approve with note. The patches are correctly applied across all build targets with the fork delta preserved. The interoperability risk is inherent to any wire-format security fix, not an error in this PR. The stopgap is appropriate given that #60 has not started. |
| var data$32 = formData; | ||
| key = nextPartId++; | ||
| var prefix = formFieldPrefix + key + "_"; | ||
| var prefix = formFieldPrefix + "_" + key + "_"; |
There was a problem hiding this comment.
Wire-format breaking change. The old prefix was formFieldPrefix + key + "_" (e.g. "_0_"); the new prefix is formFieldPrefix + "_" + key + "_" (e.g. "__0_"). The server-side $K decoder was updated in lock-step to scan for response._prefix + "_" as the outer namespace guard. Any client still on 19.0.4 sending nested FormData to a 19.0.7 server (or vice versa) will silently fail to reconstruct the FormData — keys simply won't match the expected prefix.
This is the correct fix for the CVE, but it means client and server must be updated atomically. Watch for CDN-cached old bundles during rollout.
| case "fulfilled": | ||
| id = chunk.value; | ||
| chunk = chunk.reason; | ||
| if (null !== chunk && "error" in chunk) |
There was a problem hiding this comment.
Good guard. A fulfilled chunk whose reason has an "error" property is a stream-controller object that leaked through — a symptom of an older client encoding a stream where this server expects a non-stream value. Throwing here prevents silent mis-decoding of payloads from downlevel clients and produces a clear diagnostic message instead.
| if (!isArrayImpl(model)) throw Error("Invalid Map initializer."); | ||
| if (!0 === model.$$consumed) throw Error("Already initialized Map."); | ||
| response = new Map(model); | ||
| model.$$consumed = !0; |
There was a problem hiding this comment.
Subtle but important ordering fix. Setting model.$$consumed = true before new Map(model) closes a narrow concurrent-revival window: if two revival calls race and both pass the if (!0 === model.$$consumed) check before either writes the flag, both would construct a new Map. Moving the flag write before the construction means only the first caller constructs — the second will throw "Already initialized" on the next pass. Same fix applies to createSet and extractIterator below.
| _bundlerConfig: bundlerConfig, | ||
| _prefix: formFieldPrefix, | ||
| _formData: backingFormData, | ||
| _formData: { data: backingFormData, keyPointer: -1, keys: null }, |
There was a problem hiding this comment.
Core DoS fix. Wrapping _formData in { data, keyPointer, keys } enables the $K revival path to scan keys incrementally (advancing keyPointer forward) rather than calling Array.from(response.keys()) from scratch on every nested FormData revival. The old pattern was O(n) per $K node → O(n²) total for a payload with n FormData entries, which was the DoS vector. The new scan is amortized O(n) across all revivals, and the else if (value.startsWith(obj)) break guard stops scanning once the current namespace's keys are exhausted.
Code Review: vendored react-server-dom-webpack 19.0.4 to 19.0.7OverviewThis is a security-critical vendored artifact update applying the upstream RSC reply-decoding DoS patches from React 19.0.5 and 19.0.6 to the in-tree runtime. The approach (artifact diff applied directly to CJS files, with a delta-preservation check) is the right stopgap while #60 (stock npm migration) is pending. The diff has been reviewed against upstream React 19.0.4 to 19.0.7 changes. All seven hardening changes are present and consistent across all eight build variants (browser/edge/node x production/development + unbundled x 2). Security Fixes VerifiedAll present and consistent across all 8 variants:
Notable ObservationsWire format compatibility break (expected) Delta-preservation methodology Fork-specific delta untouched Minor Issues
Implicit contiguous-key protocol invariant in
VerdictApprove. The implementation is mechanically correct, all CVE hardening markers are present post-patch, the test suite passes, and the fork deltas are cleanly preserved. The stopgap framing ("delete this when #60 lands") is appropriate. One suggestion: add a note to the CHANGELOG entry (or the release) that the wire format change requires a coordinated client+server upgrade for users pinning version mismatches. |
Greptile SummaryApplies the React 19.0.5–19.0.7 security patches (CVE-2026-23869 and CVE-2026-23870) directly to the vendored
Confidence Score: 4/5Safe to merge as a security stopgap; the patches are faithful backports of upstream React 19.0.5–19.0.7, all 16 CJS variants are consistently updated, and the fork-specific delta is preserved. The patch set is mechanically correct and complete across every browser/edge/node/unbundled dev+prod pair. The one hand-applied hunk (DevTools rendererPackageName) was preserved correctly. The vendored package.json peer dep bump to ^19.0.7 is intentionally out of step with the outer package.json's ^19.0.4 — a known deferred item — but in a mixed-version deployment where the outer package's consumers pair the new vendored server runtime against a pre-patch client, nested FormData decoding would silently return empty rather than erroring. The src/react-server-dom-webpack/package.json peer dep bump deserves a second look in the context of #60 — once the stock-npm migration lands, this file is deleted, but until then the ^19.0.4 outer constraint lets users install a React version whose client still encodes nested FormData in the old format. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Client: processReply(FormData)"] -->|"CVE-2026-23869 fix\nprefix = formFieldPrefix + '_' + key + '_'"| B["Encoded RSC Reply\n(multipart/form-data)"]
B --> C["Server: decodeReply / decodeReplyFromBusboy"]
C --> D["createResponse()\n_formData = { data, keys:null, keyPointer:-1 }"]
D --> E["resolveField(key, value)\nappend to data, update keys[]"]
E --> F{"case 'K'\n(nested FormData)"}
F -->|"CVE-2026-23870 fix\nkeyPointer scan (not full Array.from)"| G["Reconstruct nested FormData\nadvance keyPointer, stop at prefix boundary"]
G --> H["Return decoded args to Server Action"]
C --> I{"decodeReplyFromBusboy\n(node only)"}
I -->|"CVE-2026-23870 fix\nlinked-list flush()"|J["Process files in FIFO order\nqueue fields after current tail"]
J --> H
style F fill:#f9a,stroke:#c00
style I fill:#f9a,stroke:#c00
style G fill:#afa,stroke:#080
style J fill:#afa,stroke:#080
Reviews (1): Last reviewed commit: "Add changelog entry for vendored runtime..." | Re-trigger Greptile |
| if (!isArrayImpl(model)) throw Error("Invalid Map initializer."); | ||
| if (!0 === model.$$consumed) throw Error("Already initialized Map."); | ||
| response = new Map(model); | ||
| model.$$consumed = !0; |
There was a problem hiding this comment.
Security fix: $$consumed set before construction (not after)
Previously the order was:
response = new Map(model); // ← runs first
model.$$consumed = !0; // ← set second
return response;If new Map(model) threw (e.g. due to a crafted iterable that errors mid-construction), $$consumed would never be set, leaving the guard bypassable on a retry. By setting the flag first, any subsequent call — even if construction failed — will correctly raise "Already initialized Map."
This pattern is applied consistently to createMap, createSet, and extractIterator across all eight build variants.
| _bundlerConfig: bundlerConfig, | ||
| _prefix: formFieldPrefix, | ||
| _formData: backingFormData, | ||
| _formData: { data: backingFormData, keyPointer: -1, keys: null }, |
There was a problem hiding this comment.
keyPointer: -1 is dead on initialization
keyPointer is only read inside case "K" (and resolveField) when keys !== null. On the first access where keys is null, both fields are initialized together: keys = Array.from(...) and keyPointer = 0. The -1 value is never actually read.
This matches upstream 19.0.7 exactly — not introduced here. But for anyone auditing this in future, the sentinel means "not yet initialized" in the same way keys: null does, and the two are always set together. Worth a comment upstream if it ever causes confusion.
| }, | ||
| "peerDependencies": { | ||
| "react": "^19.0.4", | ||
| "react-dom": "^19.0.4", | ||
| "react": "^19.0.7", | ||
| "react-dom": "^19.0.7", | ||
| "webpack": "^5.59.0" |
There was a problem hiding this comment.
Vendored peerDeps now require
^19.0.7 while the outer package still declares ^19.0.4
The vendored package.json now declares "react": "^19.0.7" / "react-dom": "^19.0.7", but the outer package.json at the repo root still has "react": "^19.0.4". The PR notes this is intentional and deferred to #60, but it creates a subtle protocol mismatch risk for any downstream consumer that pairs the new vendored server runtime (which decodes the prefix + "_" + key + "_" format) against a stock react-server-dom-webpack@19.0.4 client (which still emits the old prefix + key + "_" format). In that mixed-version scenario, server-side decoding of nested FormData ($K values) would silently return an empty FormData rather than erroring. Raising this for visibility before #60 lands.
| var data$32 = formData; | ||
| key = nextPartId++; | ||
| var prefix = formFieldPrefix + key + "_"; | ||
| var prefix = formFieldPrefix + "_" + key + "_"; |
There was a problem hiding this comment.
Wire-format break: coordinated client+server upgrade required
The prefix separator changed from formFieldPrefix + key + "_" to formFieldPrefix + "_" + key + "_". The server-side case "K" decoder (in parseModelString) was updated symmetrically, so client and server are consistent within this patch.
However, a 19.0.4 client sending a $K-encoded FormData to a 19.0.7 server will silently produce an empty FormData on the server — the startsWith(key) check never matches the old format. There is no error, just missing data. The reverse (19.0.7 client → 19.0.4 server) has the same silent failure mode.
This is expected per the CVE fix, but the release notes should explicitly state that deployments mixing 19.0.4 and 19.0.7 packages will silently drop Server Action FormData arguments.
| arrayRoot.append(referencedFormDataKey, reference[i]); | ||
| response.data.delete(value); | ||
| response.keyPointer++; | ||
| } else if (value.startsWith(obj)) break; |
There was a problem hiding this comment.
Implicit invariant: nested-FormData keys must be contiguous
This else if break assumes that once we encounter a key that belongs to the same _prefix namespace (starts with obj) but is not for this nested FormData entry (does not start with key), all remaining keys for this FormData entry have already been consumed.
This holds because the client (processReply) always appends all field entries for a nested FormData consecutively before moving on to the next encoded value. If an adversarial client interleaved keys from multiple nested FormData objects, this loop would silently miss the later entries.
The invariant is guaranteed by the protocol (not the parser), which is correct — but a brief comment here would help security reviewers quickly verify the assumption holds.
| handle = current.file, | ||
| blob = new Blob(handle.chunks, { type: handle.mime }), | ||
| backingStore = response._formData; | ||
| response = key; |
There was a problem hiding this comment.
JSCompiler variable aliasing — response is overwritten with the field name string
response was just assigned response$jscomp$0 (the RSC response object) on line 4304, and here it is immediately reassigned to key (the form field name string). From this point on in this try block, response is the string field name — used as the FormData key on line 4310 and pushed to keys on line 4315 — while the actual response object is still accessible via response$jscomp$0.
Then on line 4318, response is re-used as a loop counter (for (response = 0; ...)).
This is correct JSCompiler output (it reuses variable slots aggressively to minimize code size), but it is the single most confusing section of the diff. Anyone auditing this code should be aware of the aliasing to avoid misreading the FormData append and key-tracking logic as operating on the response object.
The CVE-2026-23869 fix changes the nested-FormData reply wire format; flagged by review as worth stating for mixed-version pairings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review triage
All checks green: unit-tests, claude-review, CodeRabbit, Cursor Bugbot. Runtime-version bumps are maintainer-gated per AGENTS.md, so this stays ready-for-maintainer-merge rather than auto-merging. |
Code Review: React 19.0.7 RSC Reply-Decode DoS FixesOverviewThis PR correctly applies the React 19.0.4 to 19.0.7 security patch to the vendored The changes across 16 CJS files are mechanically consistent: every What the Patch DoesFive distinct hardening changes land in this bump:
CorrectnessStrengths:
Concerns: 1. Wire-format breaking change is a hard compatibility boundaryThe prefix change means any client running >=19.0.5 paired with a server running <19.0.5 (or vice versa) will silently drop all nested FormData entries. The CHANGELOG note is correct. Worth verifying that the consuming app always updates client and server atomically — if there is any CDN/SSR split-deploy scenario where cached client JS runs at the old version while the server is already updated, nested FormData payloads (Server Actions with nested FormData arguments) will silently fail. 2. Test coverage for the specific security scenariosThe test plan covers the CSS Flight hint regression and overall suite pass, but there are no tests for:
A minimal test exercising nested 3.
|
| var data$32 = formData; | ||
| key = nextPartId++; | ||
| var prefix = formFieldPrefix + key + "_"; | ||
| var prefix = formFieldPrefix + "_" + key + "_"; |
There was a problem hiding this comment.
Wire-format breaking change (CVE-2026-23869 fix): the separator "_" is now inserted between the prefix and the numeric part-ID, changing the on-the-wire field name from <prefix><id>_<key> to <prefix>_<id>_<key>. This means any client that has been bumped past 19.0.5 must pair with a server on the same version — a mismatched pair silently loses all nested FormData entries rather than throwing an error.
The same change is applied consistently across all 8 client build variants (browser/edge/node × dev/prod), which is correct.
| _bundlerConfig: bundlerConfig, | ||
| _prefix: formFieldPrefix, | ||
| _formData: backingFormData, | ||
| _formData: { data: backingFormData, keyPointer: -1, keys: null }, |
There was a problem hiding this comment.
Minor inconsistency: keyPointer is initialised to -1 here, but both resolveField and the $K decode path set it to 0 the first time the keys array is built (lazily, when keys is null). The -1 value is never read before being overwritten, so this is not a bug — keys: null is the actual first-use sentinel. Still worth noting when diffing against future upstream stock versions: if upstream uses 0 here, this will appear as a spurious delta in the delta-preservation check.
| chunk = chunk.reason; | ||
| if (null !== chunk && "error" in chunk) | ||
| throw Error( | ||
| "Expected an initialized chunk but got an initialized stream chunk instead. This payload may have been submitted by an older version of React." |
There was a problem hiding this comment.
Stream-vs-model chunk guard (new in 19.0.7): after a fulfilled model chunk is retrieved, this checks that chunk.reason (the secondary slot) is not a stream controller object. Without this guard, a payload crafted to overlap model and stream chunk IDs could confuse the decoder into reading a stream controller as a model value. The error message ("submitted by an older version of React") is intentionally user-facing to aid debugging of version mismatches.
| if (!isArrayImpl(model)) throw Error("Invalid Map initializer."); | ||
| if (!0 === model.$$consumed) throw Error("Already initialized Map."); | ||
| response = new Map(model); | ||
| model.$$consumed = !0; |
There was a problem hiding this comment.
$$consumed ordering fix: in the old code, new Map(model) was called first and the result stored in response, then model.$$consumed = !0 was set. If new Map() threw (e.g., due to a non-iterable element), the $$consumed flag would never be set, allowing a second attempt to decode the same model array. Moving the flag assignment before construction ensures the guard is set even on failure, preventing potential double-initialisation. The same pattern is applied to createSet and extractIterator.
| } | ||
| head = current.next; | ||
| } | ||
| tail = null; |
There was a problem hiding this comment.
Linked-list queue replaces the file counter (correctness improvement): the old pendingFiles counter held back all fields as soon as any upload started, even if those fields appeared in the stream after a different upload that had already completed. The linked-list (head/tail) scopes queuing to the immediately preceding in-flight upload, so fields can be flushed as soon as all uploads ahead of them in the stream are done.
tail = null here is correct — this line is only reached when the while (head !== null) loop exits, meaning all queued items have been processed. If !current.complete fires mid-loop, the early return prevents reaching this statement.
| closed || | ||
| reportGlobalError( | ||
| response, | ||
| Error("Reply finished with incomplete file part.") |
There was a problem hiding this comment.
Potential misleading error message: if flush() catches a Blob-construction error and calls busboyStream.destroy(error), it returns early with closed still false. The finish handler then also calls reportGlobalError with "Reply finished with incomplete file part." Since reportGlobalError is idempotent (a second call after the first error is a no-op), this does not cause a double-error in practice. However, debugging a Blob error in production will show only the "incomplete file part" message, not the real cause. Upstream behavior, but worth knowing.
Conflict-resolution validationUpdated this branch with Local validation on current head
GitHub checks at last poll:
Per the pr-batch merge gate, I did not merge while the current-head |
…0.7-cve-dos-fixes * origin/main: Make PR skill workflows discoverable (#88)
| ### Changed | ||
| - Ported the webpack RSC plugin from the vendored built JavaScript artifact (`src/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js`) to first-class TypeScript source at `src/webpack/RSCWebpackPlugin.ts`, preserving full behavior parity (server manifest emission, CSS/JS chunk scanning, runtime-chunk filtering, dependency-type chunk-group manifest construction with eager-import fallback, duplicate-package runtime detection, and hot-update CSS exclusion). The `./WebpackPlugin`, `./WebpackLoader`, and `./RSCReferenceDiscoveryPlugin` exports are unchanged; the vendored plugin file remains in the package but is no longer used by any export path. ([#87]) | ||
|
|
||
| ### Fixed |
There was a problem hiding this comment.
Consider using ### Security instead of ### Fixed here. Per Keep-a-Changelog convention, the Security section is specifically for CVE/vulnerability fixes and is recognised by security-scanner tooling. The current Fixed heading works, but Security is more precise given that both changes are CVE patches.
| ### Fixed | |
| ### Security |
| "react": "^19.0.7", | ||
| "react-dom": "^19.0.7", |
There was a problem hiding this comment.
Bumping the vendored peerDependencies to ^19.0.7 means npm will emit an unmet peer warning for any consumer running React 19.0.4-19.0.6. This is the stock 19.0.7 value and the PR description notes this file is "internal and not resolved by npm" — but that is only guaranteed if the shipped package excludes this package.json from its published files. It would be worth either (a) confirming the outer package.json files field omits src/react-server-dom-webpack/package.json, or (b) adding a note in the CHANGELOG/deployment docs that users on 19.0.4-19.0.6 may see a peer warning they can safely ignore until #60 lands.
| _bundlerConfig: bundlerConfig, | ||
| _prefix: formFieldPrefix, | ||
| _formData: backingFormData, | ||
| _formData: { data: backingFormData, keyPointer: -1, keys: null }, |
There was a problem hiding this comment.
Nit (non-blocking — verbatim upstream code): keyPointer: -1 is a safe sentinel because keys: null is always checked first and keyPointer is reset to 0 before it is ever read as an array index. However, keys: null already fully encodes the "uninitialized" state, so initialising keyPointer to 0 here would be equally correct and removes the off-by-one footgun if future code reads keyPointer without first checking keys !== null.
| if (null !== chunk && "error" in chunk) | ||
| throw Error( | ||
| "Expected an initialized chunk but got an initialized stream chunk instead. This payload may have been submitted by an older version of React." | ||
| ); |
There was a problem hiding this comment.
Version-mismatch guard (CVE-2026-23870): A "fulfilled" chunk that still carries an "error" property is a stream chunk, not a plain data chunk — this shape is produced by pre-19.0.5 servers using the old wire format. Throwing here prevents the decoder from misinterpreting stream-framing metadata as model data, which could otherwise cause silent data corruption or a DoS depending on what the payload contains. The error message correctly surfaces the likely root cause (mismatched runtime versions) rather than a cryptic type error deeper in the decode path.
| if (!0 === model.$$consumed) throw Error("Already initialized Map."); | ||
| response = new Map(model); | ||
| model.$$consumed = !0; |
There was a problem hiding this comment.
$$consumed ordering fix: Setting the flag before constructing new Map(model) / new Set(model) means that even if the constructor throws (e.g. OOM, or a custom iterator that raises), the guard is already armed. Under the old ordering, a throw between construction and the flag-set would leave $$consumed = false, allowing a second call to re-initialize the same model array — a potential re-use vector. This ordering is the correct, hardened form.
| busboyStream.on("finish", function () { | ||
| close(response); | ||
| bodyFinished = !0; | ||
| flush(); | ||
| closed || | ||
| reportGlobalError( | ||
| response, | ||
| Error("Reply finished with incomplete file part.") | ||
| ); |
There was a problem hiding this comment.
Truncated-upload detection: Under the old code, busboyStream.on("finish") called close(response) unconditionally, so a connection that dropped mid-file would silently close the response with whatever partial data was already resolved. The new logic only calls close (via flush) if closed was already set to true inside the loop — meaning all pending files completed and were processed. If any file node is still complete: false when finish fires, closed remains false and this error path fires, correctly propagating a fatal error to the response rather than silently truncating.
One edge case to be aware of: if flush() itself calls busboyStream.destroy(error) on a processing error, the "error" event will fire after "finish", so reportGlobalError may be called twice (once from flush and once from the "error" handler). The second call is benign since reportGlobalError on an already-errored response is a no-op in React's error handling.
Current-head review triageCurrent head: Local validation on this head:
GitHub checks on this head:
New current-head Claude feedback disposition:
No current-head blocker remains from review feedback. |
…0.7-cve-dos-fixes * origin/main: Harden dependency and Claude automation (#76)
| ### Changed | ||
| - Ported the webpack RSC plugin from the vendored built JavaScript artifact (`src/react-server-dom-webpack/cjs/react-server-dom-webpack-plugin.js`) to first-class TypeScript source at `src/webpack/RSCWebpackPlugin.ts`, preserving full behavior parity (server manifest emission, CSS/JS chunk scanning, runtime-chunk filtering, dependency-type chunk-group manifest construction with eager-import fallback, duplicate-package runtime detection, and hot-update CSS exclusion). The `./WebpackPlugin`, `./WebpackLoader`, and `./RSCReferenceDiscoveryPlugin` exports are unchanged; the vendored plugin file remains in the package but is no longer used by any export path. ([#87]) | ||
|
|
||
| ### Fixed |
There was a problem hiding this comment.
Consider using ### Security instead of ### Fixed here. Per Keep-a-Changelog convention, the Security section is specifically for CVE/vulnerability fixes and is recognised by security-scanner tooling. The current Fixed heading works, but Security is more precise given that both changes are CVE patches.
| ### Fixed | |
| ### Security |
| "react": "^19.0.7", | ||
| "react-dom": "^19.0.7", |
There was a problem hiding this comment.
Bumping the vendored peerDependencies to ^19.0.7 means npm will emit an unmet peer warning for any consumer running React 19.0.4-19.0.6. This is the stock 19.0.7 value and the PR description notes this file is "internal and not resolved by npm" — but that is only guaranteed if the shipped package excludes this package.json from its published files. It would be worth either (a) confirming the outer package.json files field omits src/react-server-dom-webpack/package.json, or (b) adding a note in the CHANGELOG/deployment docs that users on 19.0.4-19.0.6 may see a peer warning they can safely ignore until #60 lands.
| busboyStream.destroy(error); | ||
| return; |
There was a problem hiding this comment.
After busboyStream.destroy(error), the function returns without advancing head = current.next. If another file's end event fires before destroy() fully suppresses events (edge case in the same microtask), flush() would re-enter and retry the same failed node. Busboy's destroy() is reliable in practice, so this is safe — and this code is verbatim from upstream React 19.0.7 — but the gap is worth noting for anyone who later adapts flush() to a different stream implementation.
Code Review: Apply React 19.0.7 RSC reply-decode DoS fixes (see inline comments for details) |
Code Review: Apply React 19.0.7 RSC reply-decode DoS fixesPlease see the three inline comments posted on this PR for specific line-level feedback. OverviewThis PR applies the upstream React 19.0.5-19.0.7 security patches to the vendored The changes are applied across all 12 server build variants and all 8 client build variants. The delta-preservation claim (stock 19.0.7 + pre-existing fork deltas) appears sound based on the diff. What the patch doesClient (all 8 variants — wire format fix for CVE-2026-23869): Server (all 12 variants — CVE-2026-23869 + CVE-2026-23870):
Node-only ( Issues1. CHANGELOG section heading — minor (see inline comment on CHANGELOG.md line 10) 2. Vendored 3. 4. Double What is correct and well-handled
VerdictApprove with minor notes. The security patches are faithfully applied and the core logic is correct. Items 1–2 are worth addressing before the release tag; items 3–4 are informational observations about upstream behaviour inherited verbatim. |
Code Review: React 19.0.7 CVE patch on vendored runtimeOverviewThis is a well-executed security backport. The diff is faithful to upstream 19.0.7, all 16 vendored CJS variants are updated consistently, the fork-specific What the patch does (summary for reviewers)
Issues1. Test gap:
|
| if (!isArrayImpl(model)) throw Error("Invalid Map initializer."); | ||
| if (!0 === model.$$consumed) throw Error("Already initialized Map."); | ||
| response = new Map(model); | ||
| model.$$consumed = !0; |
There was a problem hiding this comment.
Correct fix for the $$consumed double-initialization vulnerability.
Old order:
response = new Map(model); // construct first
model.$$consumed = !0; // mark consumed after — if constructor throws, flag never setNew order (this PR):
model.$$consumed = !0; // mark consumed first — even if constructor throws, retry is blocked
return new Map(model);The old order allowed an attacker to craft a payload that caused new Map(model) to throw, then retry the same model since $$consumed was never set. The new order marks the model consumed before any potentially-throwing operation, closing that window. The tradeoff — if construction legitimately fails, the model is silently marked consumed and the value is lost — is the correct security choice here since this is untrusted input decoding.
| arrayRoot.append(referencedFormDataKey, reference[i]); | ||
| response.data.delete(value); | ||
| response.keyPointer++; | ||
| } else if (value.startsWith(obj)) break; |
There was a problem hiding this comment.
This early-break is the core DoS mitigation for CVE-2026-23869 — correct but subtle.
The loop processes FormData keys in order using a pointer. When it hits a key that starts with obj (the overall prefix, e.g. "0_") but NOT with key (the specific FormData prefix, e.g. "0_3_"), it breaks. This works because the client's encodeReply always emits all keys for a given nested FormData contiguously — so once you see a key for a different FormData part (same prefix, different ID), there are no more keys for the current one.
The security win: the old code did Array.from(response.keys()) materializing the entire key set into memory up front, then scanned every entry. An attacker could stuff thousands of unrelated keys into the form to make every "K" decode O(n). The new pointer scan stops early and pays O(matching keys + 1 lookahead).
Edge case: a crafted payload with interleaved keys (keys for FormData A, then FormData B, then FormData A again) would silently truncate FormData A's second batch rather than crash. This is acceptable — legitimate clients never produce this ordering.
Current-head review triage addendumCurrent head: Latest Claude review comments after the prior triage are non-blocking:
No current-head review blocker remains. |
Merge criteria satisfied: full GitHub check list green (unit-tests, e2e-tests, verify-artifacts, claude-review, Greptile, Cursor Bugbot, CodeRabbit; Claude workflow skipped), mergeStateStatus CLEAN / mergeable MERGEABLE, and all review threads resolved or explicitly triaged. Local evidence: yarn install --frozen-lockfile, yarn verify:artifacts, yarn build, git diff --check. This restores main health after the #77 artifact gate exposed the #86 runtime/root-peer policy mismatch; no publish, tag, release, or registry mutation was run.
* origin/main: Add release artifact verification gate (#77) Apply React 19.0.7 RSC reply-decode DoS fixes (CVE-2026-23869, CVE-2026-23870) to vendored runtime (#86) Harden dependency and Claude automation (#76) Make PR skill workflows discoverable (#88) Extract and own the webpack RSC plugin as TypeScript source (#87) Add packed-tarball E2E pipeline suite: webpack+rspack → Flight → SSR HTML → hydration (#85) Docs: Option 5 stock npm runtime go/no-go decision (#55 spike) (#80)
…h-tests * origin/main: Add release artifact verification gate (#77) Apply React 19.0.7 RSC reply-decode DoS fixes (CVE-2026-23869, CVE-2026-23870) to vendored runtime (#86) Harden dependency and Claude automation (#76) Make PR skill workflows discoverable (#88) Extract and own the webpack RSC plugin as TypeScript source (#87) Add packed-tarball E2E pipeline suite: webpack+rspack → Flight → SSR HTML → hydration (#85) Docs: Option 5 stock npm runtime go/no-go decision (#55 spike) (#80)
…atrix * origin/main: Allow artifact verifier to accept covering root peer ranges (#95) Add release artifact verification gate (#77) Apply React 19.0.7 RSC reply-decode DoS fixes (CVE-2026-23869, CVE-2026-23870) to vendored runtime (#86) Harden dependency and Claude automation (#76)
* origin/main: Add compatibility matrix and canary signal (#83) Document runtime versioning policy and live backlog pointers (#79) Add Flight client error path coverage (#78) Document release dist-tag policy and artifact parity checks (#75) Allow artifact verifier to accept covering root peer ranges (#95) Add release artifact verification gate (#77) Apply React 19.0.7 RSC reply-decode DoS fixes (CVE-2026-23869, CVE-2026-23870) to vendored runtime (#86) Harden dependency and Claude automation (#76) Make PR skill workflows discoverable (#88) Extract and own the webpack RSC plugin as TypeScript source (#87) Add packed-tarball E2E pipeline suite: webpack+rspack → Flight → SSR HTML → hydration (#85) Docs: Option 5 stock npm runtime go/no-go decision (#55 spike) (#80) # Conflicts: # docs/releasing.md
…ase-motion * origin/main: Release from GitHub Actions with trusted publishing (#84) Add Claude agent setup and workflow skill stubs (#82) Add compatibility matrix and canary signal (#83) Document runtime versioning policy and live backlog pointers (#79) Add Flight client error path coverage (#78) Document release dist-tag policy and artifact parity checks (#75) Allow artifact verifier to accept covering root peer ranges (#95) Add release artifact verification gate (#77) Apply React 19.0.7 RSC reply-decode DoS fixes (CVE-2026-23869, CVE-2026-23870) to vendored runtime (#86) Harden dependency and Claude automation (#76) Make PR skill workflows discoverable (#88) Extract and own the webpack RSC plugin as TypeScript source (#87) Add packed-tarball E2E pipeline suite: webpack+rspack → Flight → SSR HTML → hydration (#85) Docs: Option 5 stock npm runtime go/no-go decision (#55 spike) (#80) # Conflicts: # CHANGELOG.md
Summary
Brings the vendored
react-server-dom-webpackruntime (src/react-server-dom-webpack/) from the 19.0.4 security level up to 19.0.7, picking up the two 2026 React Server Components reply-decoding DoS fixes:>=19.0.0 <19.0.5, patched upstream in 19.0.5>=19.0.0 <19.0.6, patched upstream in 19.0.6Approach
Per the #55 spike decision, the vendored runtime is slated for replacement with stock npm
react-server-dom-webpackin #60 — but that migration has not started, so this is the interim fix. Instead of rebasing theabanoubghadban/reactfork patches onto a newer upstream commit, this applies the full npm 19.0.4 → 19.0.7 built-artifact diff (extracted vianpm pack) directly to the vendored cjs files, mirroring how dc4dfb4/867634f previously patched built artifacts in place.The 19.0.x line is security-only, so the entire diff is the reply-decode hardening (FormData access wrapper with key-pointer scanning, form-field prefix separator change, "initialized stream chunk" guard,
$$consumedordering) plus version strings. 16 of 17 hunk sets applied cleanly withpatch; the one reject (DevTools registration block inclient.browser.development.js, where the fork setsrendererPackageName: "react-on-rails-rsc") was applied by hand preserving the fork value.Delta-preservation check: for every vendored cjs file,
diff(stock 19.0.7, vendored-after)is byte-identical todiff(stock 19.0.4, vendored-before)— i.e. the runtime is now exactly stock 19.0.7 plus the pre-existing in-repo deltas (Flight CSS hint emission from dc4dfb4/867634f/8781f08, and the fork's bound-args +rendererPackageNamechanges). Nothing else moved.The vendored
package.json(previously byte-identical to stock 19.0.4) takes the stock 19.0.7 version/peerDeps bump; it is internal to the shipped package and not resolved by npm. The outer package's peerDependencies are untouched (that belongs to #60).esm/only contains the node loader, which is unchanged between 19.0.4 and 19.0.7.Relationship to #60
This does not close #60 — it is the stopgap that keeps released artifacts off the vulnerable runtime until the stock-npm migration lands. If #60 lands soon, this diff is simply deleted along with the vendored tree.
Test plan
yarn build— passes (tsc clean)yarn test— 17 suites / 158 tests pass (matches baseline)NODE_CONDITIONS=react-server yarn jest tests/react-flight-client-reference-css.rsc.test.ts— passes (FOUC CSS Flight-hint behavior from dc4dfb4/867634f preserved)_formData.data.getand the "initialized stream chunk" guard now appear in the vendored server builds (previously 0 hits)Deployment note (split client/server rollout)
The CVE-2026-23869 fix changes the reply wire format for nested
FormData: field keys gain a"_"separator between the form-field prefix and the part ID. Client (encodeReply) and server (decodeReply/decodeReplyFromBusboy) are updated in lock-step in this PR, but operators who cache or CDN-serve client JS separately from the server runtime should deploy both together — a pre-19.0.5 runtime on either side silently drops nestedFormDataentries (no error). Plain values, Maps, Sets, and non-nested forms are unaffected. This is also noted in the CHANGELOG entry.Post-review smoke test (nested FormData round-trip)
Per review suggestion, verified the patched
$Kpath end-to-end against the builtdist/runtime (node --conditions=react-server):encodeReply([FormData, 'plain', FormData, Map, Set])fromclient.browseremits the new wire format (_1_alpha,_1_beta,_2_gamma— leading_separator present), anddecodeReplyfromserver.nodereconstructs both nestedFormDataobjects with all entries plus the Map/Set/plain values intact:🤖 Generated with Claude Code
Note
High Risk
Security-critical RSC reply decoding and a breaking nested FormData wire format; mismatched client/server versions can lose data without errors.
Overview
Bumps the vendored react-server-dom-webpack runtime from 19.0.4 to 19.0.7, bringing in upstream reply-decoding hardening for CVE-2026-23869 and CVE-2026-23870 (DoS in RSC reply parsing). The CHANGELOG documents the security fix and a deployment caveat for nested FormData.
Client
encodeReplynow prefixes nested FormData field keys with an extra_(prefix + "_" + key + "_"instead ofprefix + key + "_"). ServerdecodeReplypaths wrap backing FormData in{ data, keyPointer, keys }, scan keys with a pointer instead of materializing all keys at once, add an “initialized stream chunk” guard on fulfilled chunks, and reworkdecodeReplyFromBusboyto queue file parts in order before flushing. Version strings and the vendored package.json move to 19.0.7 /^19.0.7peers.Client and server must ship together on this package; mixing with pre-19.0.5 runtimes can silently drop nested FormData entries. In-repo fork deltas (e.g. Flight CSS hints,
rendererPackageName: "react-on-rails-rsc") are preserved per the PR approach.Reviewed by Cursor Bugbot for commit 25bba9f. Bugbot is set up for automated code reviews on this repo. Configure here.