Commit 63d61c7

Browse files
sebmarkbagegnofflubieowoceunstubbable
committed
Add more DoS mitigations to React Flight Reply, and harden React Flight
Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
1 parent 612e371 commit 63d61c7

17 files changed

Lines changed: 853 additions & 267 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
9898

9999
import{getOwnerStackByComponentInfoInDev}from'shared/ReactComponentInfoStack';
100100

101+
importhasOwnPropertyfrom'shared/hasOwnProperty';
102+
101103
import{injectInternals}from'./ReactFlightClientDevToolsHook';
102104

103105
import{OMITTED_PROP_ERROR}from'shared/ReactFlightPropertyAccess';
@@ -163,6 +165,8 @@ const INITIALIZED = 'fulfilled';
163165
constERRORED='rejected';
164166
constHALTED='halted';// DEV-only. Means it never resolves even if connection closes.
165167

168+
const__PROTO__='__proto__';
169+
166170
typePendingChunk<T>={
167171
status: 'pending',
168172
value: null|Array<InitializationReference|(T=>mixed)>,
@@ -1496,7 +1500,16 @@ function fulfillReference(
14961500
}
14971501
}
14981502
}
1499-
value=value[path[i]];
1503+
constname=path[i];
1504+
if(
1505+
typeofvalue=== 'object' &&
1506+
value!==null&&
1507+
hasOwnProperty.call(value,name)
1508+
){
1509+
value=value[name];
1510+
} else {
1511+
thrownewError('Invalid reference.');
1512+
}
15001513
}
15011514

15021515
while(
@@ -1532,7 +1545,9 @@ function fulfillReference(
15321545
}
15331546

15341547
constmappedValue=map(response,value,parentObject,key);
1535-
parentObject[key]=mappedValue;
1548+
if(key!==__PROTO__){
1549+
parentObject[key]=mappedValue;
1550+
}
15361551

15371552
// If this is the root object for a model reference, where `handler.value`
15381553
// is a stale `null`, the resolved value can be used directly.
@@ -1799,7 +1814,9 @@ function loadServerReference<A: Iterable<any>, T>(
17991814
response._encodeFormAction,
18001815
);
18011816

1802-
parentObject[key]=resolvedValue;
1817+
if(key!==__PROTO__){
1818+
parentObject[key]=resolvedValue;
1819+
}
18031820

18041821
// If this is the root object for a model reference, where `handler.value`
18051822
// is a stale `null`, the resolved value can be used directly.
@@ -2177,29 +2194,31 @@ function defineLazyGetter<T>(
21772194
): any {
21782195
// We don't immediately initialize it even if it's resolved.
21792196
// Instead, we wait for the getter to get accessed.
2180-
Object.defineProperty(parentObject,key,{
2181-
get: function(){
2182-
if(chunk.status===RESOLVED_MODEL){
2183-
// If it was now resolved, then we initialize it. This may then discover
2184-
// a new set of lazy references that are then asked for eagerly in case
2185-
// we get that deep.
2186-
initializeModelChunk(chunk);
2187-
}
2188-
switch(chunk.status){
2189-
caseINITIALIZED: {
2190-
returnchunk.value;
2197+
if(key!==__PROTO__){
2198+
Object.defineProperty(parentObject,key,{
2199+
get: function(){
2200+
if(chunk.status===RESOLVED_MODEL){
2201+
// If it was now resolved, then we initialize it. This may then discover
2202+
// a new set of lazy references that are then asked for eagerly in case
2203+
// we get that deep.
2204+
initializeModelChunk(chunk);
21912205
}
2192-
caseERRORED:
2193-
throwchunk.reason;
2194-
}
2195-
// Otherwise, we didn't have enough time to load the object before it was
2196-
// accessed or the connection closed. So we just log that it was omitted.
2197-
// TODO: We should ideally throw here to indicate a difference.
2198-
returnOMITTED_PROP_ERROR;
2199-
},
2200-
enumerable: true,
2201-
configurable: false,
2202-
});
2206+
switch(chunk.status){
2207+
caseINITIALIZED: {
2208+
returnchunk.value;
2209+
}
2210+
caseERRORED:
2211+
throwchunk.reason;
2212+
}
2213+
// Otherwise, we didn't have enough time to load the object before it was
2214+
// accessed or the connection closed. So we just log that it was omitted.
2215+
// TODO: We should ideally throw here to indicate a difference.
2216+
returnOMITTED_PROP_ERROR;
2217+
},
2218+
enumerable: true,
2219+
configurable: false,
2220+
});
2221+
}
22032222
return null;
22042223
}
22052224

@@ -2510,14 +2529,16 @@ function parseModelString(
25102529
// In DEV mode we encode omitted objects in logs as a getter that throws
25112530
// so that when you try to access it on the client, you know why that
25122531
// happened.
2513-
Object.defineProperty(parentObject,key,{
2514-
get: function(){
2515-
// TODO: We should ideally throw here to indicate a difference.
2516-
returnOMITTED_PROP_ERROR;
2517-
},
2518-
enumerable: true,
2519-
configurable: false,
2520-
});
2532+
if(key!==__PROTO__){
2533+
Object.defineProperty(parentObject,key,{
2534+
get: function(){
2535+
// TODO: We should ideally throw here to indicate a difference.
2536+
returnOMITTED_PROP_ERROR;
2537+
},
2538+
enumerable: true,
2539+
configurable: false,
2540+
});
2541+
}
25212542
returnnull;
25222543
}
25232544
// Fallthrough
@@ -5143,6 +5164,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
51435164
function createFromJSONCallback(response: Response) {
51445165
// $FlowFixMe[missing-this-annot]
51455166
returnfunction(key: string,value: JSONValue){
5167+
if(key===__PROTO__){
5168+
returnundefined;
5169+
}
51465170
if(typeofvalue==='string'){
51475171
// We can't use .bind here because we need the "this" value.
51485172
returnparseModelString(response,this,key,value);

‎packages/react-client/src/ReactFlightReplyClient.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export type ReactServerValue =
9595

9696
type ReactServerObject = {+[key: string]: ReactServerValue};
9797

98+
const __PROTO__ = '__proto__';
99+
98100
function serializeByValueID(id: number): string {
99101
return'$'+id.toString(16);
100102
}
@@ -361,6 +363,15 @@ export function processReply(
361363
): ReactJSONValue {
362364
constparent=this;
363365

366+
if(__DEV__){
367+
if(key===__PROTO__){
368+
console.error(
369+
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
370+
describeObjectForErrorMessage(parent,key),
371+
);
372+
}
373+
}
374+
364375
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
365376
if(__DEV__){
366377
// $FlowFixMe[incompatible-use]
@@ -780,6 +791,10 @@ export function processReply(
780791
if (typeof value === 'function') {
781792
constreferenceClosure=knownServerReferences.get(value);
782793
if(referenceClosure!==undefined){
794+
const existingReference =writtenObjects.get(value);
795+
if(existingReference!==undefined){
796+
return existingReference;
797+
}
783798
const{id, bound}=referenceClosure;
784799
constreferenceClosureJSON=JSON.stringify({id, bound},resolveToJSON);
785800
if(formData===null){
@@ -789,7 +804,10 @@ export function processReply(
789804
// The reference to this function came from the same client so we can pass it back.
790805
constrefId=nextPartId++;
791806
formData.set(formFieldPrefix+refId,referenceClosureJSON);
792-
returnserializeServerReferenceID(refId);
807+
constserverReferenceId=serializeServerReferenceID(refId);
808+
// Store the server reference ID for deduplication.
809+
writtenObjects.set(value,serverReferenceId);
810+
returnserverReferenceId;
793811
}
794812
if (temporaryReferences !== undefined &&key.indexOf(':')===-1){
795813
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.

‎packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,17 @@ function prerenderToNodeStream(
332332
functiondecodeReplyFromBusboy<T>(
333333
busboyStream: Busboy,
334334
moduleBasePath: ServerManifest,
335-
options?: {temporaryReferences?: TemporaryReferenceSet},
335+
options?: {
336+
temporaryReferences?: TemporaryReferenceSet,
337+
arraySizeLimit?: number,
338+
},
336339
): Thenable<T>{
337340
const response =createResponse(
338341
moduleBasePath,
339342
'',
340343
options ? options.temporaryReferences : undefined,
344+
undefined,
345+
options ? options.arraySizeLimit : undefined,
341346
);
342347
letpendingFiles=0;
343348
constqueuedFields: Array<string>=[];
@@ -403,7 +408,10 @@ function decodeReplyFromBusboy<T>(
403408
functiondecodeReply<T>(
404409
body: string|FormData,
405410
moduleBasePath: ServerManifest,
406-
options?: {temporaryReferences?: TemporaryReferenceSet},
411+
options?: {
412+
temporaryReferences?: TemporaryReferenceSet,
413+
arraySizeLimit?: number,
414+
},
407415
): Thenable<T>{
408416
if(typeofbody=== 'string'){
409417
constform=newFormData();
@@ -415,6 +423,7 @@ function decodeReply<T>(
415423
'',
416424
options ? options.temporaryReferences : undefined,
417425
body,
426+
options ? options.arraySizeLimit : undefined,
418427
);
419428
constroot=getRoot<T>(response);
420429
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ export function registerServerActions(manifest: ServerManifest) {
248248

249249
exportfunctiondecodeReply<T>(
250250
body: string|FormData,
251-
options?: {temporaryReferences?: TemporaryReferenceSet},
251+
options?: {
252+
temporaryReferences?: TemporaryReferenceSet,
253+
arraySizeLimit?: number,
254+
},
252255
): Thenable<T>{
253256
if(typeofbody==='string'){
254257
constform=newFormData();
@@ -260,6 +263,7 @@ export function decodeReply<T>(
260263
'',
261264
options ? options.temporaryReferences : undefined,
262265
body,
266+
options ? options.arraySizeLimit : undefined,
263267
);
264268
constroot=getRoot<T>(response);
265269
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ export function registerServerActions(manifest: ServerManifest) {
253253

254254
exportfunctiondecodeReply<T>(
255255
body: string|FormData,
256-
options?: {temporaryReferences?: TemporaryReferenceSet},
256+
options?: {
257+
temporaryReferences?: TemporaryReferenceSet,
258+
arraySizeLimit?: number,
259+
},
257260
): Thenable<T>{
258261
if(typeofbody==='string'){
259262
constform=newFormData();
@@ -265,6 +268,7 @@ export function decodeReply<T>(
265268
'',
266269
options ? options.temporaryReferences : undefined,
267270
body,
271+
options ? options.arraySizeLimit : undefined,
268272
);
269273
constroot=getRoot<T>(response);
270274
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,17 @@ export function registerServerActions(manifest: ServerManifest) {
562562

563563
exportfunctiondecodeReplyFromBusboy<T>(
564564
busboyStream: Busboy,
565-
options?: {temporaryReferences?: TemporaryReferenceSet},
565+
options?: {
566+
temporaryReferences?: TemporaryReferenceSet,
567+
arraySizeLimit?: number,
568+
},
566569
): Thenable<T>{
567570
const response =createResponse(
568571
serverManifest,
569572
'',
570573
options ? options.temporaryReferences : undefined,
574+
undefined,
575+
options ? options.arraySizeLimit : undefined,
571576
);
572577
letpendingFiles=0;
573578
constqueuedFields: Array<string>=[];
@@ -632,7 +637,10 @@ export function decodeReplyFromBusboy<T>(
632637

633638
exportfunctiondecodeReply<T>(
634639
body: string|FormData,
635-
options?: {temporaryReferences?: TemporaryReferenceSet},
640+
options?: {
641+
temporaryReferences?: TemporaryReferenceSet,
642+
arraySizeLimit?: number,
643+
},
636644
): Thenable<T>{
637645
if(typeofbody=== 'string'){
638646
constform=newFormData();
@@ -644,6 +652,7 @@ export function decodeReply<T>(
644652
'',
645653
options ? options.temporaryReferences : undefined,
646654
body,
655+
options ? options.arraySizeLimit : undefined,
647656
);
648657
constroot=getRoot<T>(response);
649658
close(response);
@@ -652,7 +661,10 @@ export function decodeReply<T>(
652661

653662
exportfunctiondecodeReplyFromAsyncIterable<T>(
654663
iterable: AsyncIterable<[string,string|File]>,
655-
options?: {temporaryReferences?: TemporaryReferenceSet},
664+
options?: {
665+
temporaryReferences?: TemporaryReferenceSet,
666+
arraySizeLimit?: number,
667+
},
656668
): Thenable<T>{
657669
constiterator: AsyncIterator<[string,string|File]>=
658670
iterable[ASYNC_ITERATOR]();
@@ -661,6 +673,8 @@ export function decodeReplyFromAsyncIterable<T>(
661673
serverManifest,
662674
'',
663675
options ? options.temporaryReferences : undefined,
676+
undefined,
677+
options ? options.arraySizeLimit : undefined,
664678
);
665679

666680
functionprogress(

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ function prerender(
242242
functiondecodeReply<T>(
243243
body: string|FormData,
244244
turbopackMap: ServerManifest,
245-
options?: {temporaryReferences?: TemporaryReferenceSet},
245+
options?: {
246+
temporaryReferences?: TemporaryReferenceSet,
247+
arraySizeLimit?: number,
248+
},
246249
): Thenable<T>{
247250
if(typeofbody==='string'){
248251
constform=newFormData();
@@ -254,6 +257,7 @@ function decodeReply<T>(
254257
'',
255258
options ? options.temporaryReferences : undefined,
256259
body,
260+
options ? options.arraySizeLimit : undefined,
257261
);
258262
constroot=getRoot<T>(response);
259263
close(response);

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ function prerender(
247247
functiondecodeReply<T>(
248248
body: string|FormData,
249249
turbopackMap: ServerManifest,
250-
options?: {temporaryReferences?: TemporaryReferenceSet},
250+
options?: {
251+
temporaryReferences?: TemporaryReferenceSet,
252+
arraySizeLimit?: number,
253+
},
251254
): Thenable<T>{
252255
if(typeofbody==='string'){
253256
constform=newFormData();
@@ -259,6 +262,7 @@ function decodeReply<T>(
259262
'',
260263
options ? options.temporaryReferences : undefined,
261264
body,
265+
options ? options.arraySizeLimit : undefined,
262266
);
263267
constroot=getRoot<T>(response);
264268
close(response);
@@ -268,7 +272,10 @@ function decodeReply<T>(
268272
functiondecodeReplyFromAsyncIterable<T>(
269273
iterable: AsyncIterable<[string,string|File]>,
270274
turbopackMap: ServerManifest,
271-
options?: {temporaryReferences?: TemporaryReferenceSet},
275+
options?: {
276+
temporaryReferences?: TemporaryReferenceSet,
277+
arraySizeLimit?: number,
278+
},
272279
): Thenable<T>{
273280
constiterator: AsyncIterator<[string,string|File]>=
274281
iterable[ASYNC_ITERATOR]();
@@ -277,6 +284,8 @@ function decodeReplyFromAsyncIterable<T>(
277284
turbopackMap,
278285
'',
279286
options ? options.temporaryReferences : undefined,
287+
undefined,
288+
options ? options.arraySizeLimit : undefined,
280289
);
281290

282291
functionprogress(

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit 63d61c7

Browse files
sebmarkbagegnofflubieowoceunstubbable
committed
Add more DoS mitigations to React Flight Reply, and harden React Flight
Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
1 parent 612e371 commit 63d61c7

17 files changed

Lines changed: 853 additions & 267 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
9898

9999
import{getOwnerStackByComponentInfoInDev}from'shared/ReactComponentInfoStack';
100100

101+
importhasOwnPropertyfrom'shared/hasOwnProperty';
102+
101103
import{injectInternals}from'./ReactFlightClientDevToolsHook';
102104

103105
import{OMITTED_PROP_ERROR}from'shared/ReactFlightPropertyAccess';
@@ -163,6 +165,8 @@ const INITIALIZED = 'fulfilled';
163165
constERRORED='rejected';
164166
constHALTED='halted';// DEV-only. Means it never resolves even if connection closes.
165167

168+
const__PROTO__='__proto__';
169+
166170
typePendingChunk<T>={
167171
status: 'pending',
168172
value: null|Array<InitializationReference|(T=>mixed)>,
@@ -1496,7 +1500,16 @@ function fulfillReference(
14961500
}
14971501
}
14981502
}
1499-
value=value[path[i]];
1503+
constname=path[i];
1504+
if(
1505+
typeofvalue=== 'object' &&
1506+
value!==null&&
1507+
hasOwnProperty.call(value,name)
1508+
){
1509+
value=value[name];
1510+
} else {
1511+
thrownewError('Invalid reference.');
1512+
}
15001513
}
15011514

15021515
while(
@@ -1532,7 +1545,9 @@ function fulfillReference(
15321545
}
15331546

15341547
constmappedValue=map(response,value,parentObject,key);
1535-
parentObject[key]=mappedValue;
1548+
if(key!==__PROTO__){
1549+
parentObject[key]=mappedValue;
1550+
}
15361551

15371552
// If this is the root object for a model reference, where `handler.value`
15381553
// is a stale `null`, the resolved value can be used directly.
@@ -1799,7 +1814,9 @@ function loadServerReference<A: Iterable<any>, T>(
17991814
response._encodeFormAction,
18001815
);
18011816

1802-
parentObject[key]=resolvedValue;
1817+
if(key!==__PROTO__){
1818+
parentObject[key]=resolvedValue;
1819+
}
18031820

18041821
// If this is the root object for a model reference, where `handler.value`
18051822
// is a stale `null`, the resolved value can be used directly.
@@ -2177,29 +2194,31 @@ function defineLazyGetter<T>(
21772194
): any {
21782195
// We don't immediately initialize it even if it's resolved.
21792196
// Instead, we wait for the getter to get accessed.
2180-
Object.defineProperty(parentObject,key,{
2181-
get: function(){
2182-
if(chunk.status===RESOLVED_MODEL){
2183-
// If it was now resolved, then we initialize it. This may then discover
2184-
// a new set of lazy references that are then asked for eagerly in case
2185-
// we get that deep.
2186-
initializeModelChunk(chunk);
2187-
}
2188-
switch(chunk.status){
2189-
caseINITIALIZED: {
2190-
returnchunk.value;
2197+
if(key!==__PROTO__){
2198+
Object.defineProperty(parentObject,key,{
2199+
get: function(){
2200+
if(chunk.status===RESOLVED_MODEL){
2201+
// If it was now resolved, then we initialize it. This may then discover
2202+
// a new set of lazy references that are then asked for eagerly in case
2203+
// we get that deep.
2204+
initializeModelChunk(chunk);
21912205
}
2192-
caseERRORED:
2193-
throwchunk.reason;
2194-
}
2195-
// Otherwise, we didn't have enough time to load the object before it was
2196-
// accessed or the connection closed. So we just log that it was omitted.
2197-
// TODO: We should ideally throw here to indicate a difference.
2198-
returnOMITTED_PROP_ERROR;
2199-
},
2200-
enumerable: true,
2201-
configurable: false,
2202-
});
2206+
switch(chunk.status){
2207+
caseINITIALIZED: {
2208+
returnchunk.value;
2209+
}
2210+
caseERRORED:
2211+
throwchunk.reason;
2212+
}
2213+
// Otherwise, we didn't have enough time to load the object before it was
2214+
// accessed or the connection closed. So we just log that it was omitted.
2215+
// TODO: We should ideally throw here to indicate a difference.
2216+
returnOMITTED_PROP_ERROR;
2217+
},
2218+
enumerable: true,
2219+
configurable: false,
2220+
});
2221+
}
22032222
return null;
22042223
}
22052224

@@ -2510,14 +2529,16 @@ function parseModelString(
25102529
// In DEV mode we encode omitted objects in logs as a getter that throws
25112530
// so that when you try to access it on the client, you know why that
25122531
// happened.
2513-
Object.defineProperty(parentObject,key,{
2514-
get: function(){
2515-
// TODO: We should ideally throw here to indicate a difference.
2516-
returnOMITTED_PROP_ERROR;
2517-
},
2518-
enumerable: true,
2519-
configurable: false,
2520-
});
2532+
if(key!==__PROTO__){
2533+
Object.defineProperty(parentObject,key,{
2534+
get: function(){
2535+
// TODO: We should ideally throw here to indicate a difference.
2536+
returnOMITTED_PROP_ERROR;
2537+
},
2538+
enumerable: true,
2539+
configurable: false,
2540+
});
2541+
}
25212542
returnnull;
25222543
}
25232544
// Fallthrough
@@ -5143,6 +5164,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
51435164
function createFromJSONCallback(response: Response) {
51445165
// $FlowFixMe[missing-this-annot]
51455166
returnfunction(key: string,value: JSONValue){
5167+
if(key===__PROTO__){
5168+
returnundefined;
5169+
}
51465170
if(typeofvalue==='string'){
51475171
// We can't use .bind here because we need the "this" value.
51485172
returnparseModelString(response,this,key,value);

‎packages/react-client/src/ReactFlightReplyClient.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export type ReactServerValue =
9595

9696
type ReactServerObject = {+[key: string]: ReactServerValue};
9797

98+
const __PROTO__ = '__proto__';
99+
98100
function serializeByValueID(id: number): string {
99101
return'$'+id.toString(16);
100102
}
@@ -361,6 +363,15 @@ export function processReply(
361363
): ReactJSONValue {
362364
constparent=this;
363365

366+
if(__DEV__){
367+
if(key===__PROTO__){
368+
console.error(
369+
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
370+
describeObjectForErrorMessage(parent,key),
371+
);
372+
}
373+
}
374+
364375
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
365376
if(__DEV__){
366377
// $FlowFixMe[incompatible-use]
@@ -780,6 +791,10 @@ export function processReply(
780791
if (typeof value === 'function') {
781792
constreferenceClosure=knownServerReferences.get(value);
782793
if(referenceClosure!==undefined){
794+
const existingReference =writtenObjects.get(value);
795+
if(existingReference!==undefined){
796+
return existingReference;
797+
}
783798
const{id, bound}=referenceClosure;
784799
constreferenceClosureJSON=JSON.stringify({id, bound},resolveToJSON);
785800
if(formData===null){
@@ -789,7 +804,10 @@ export function processReply(
789804
// The reference to this function came from the same client so we can pass it back.
790805
constrefId=nextPartId++;
791806
formData.set(formFieldPrefix+refId,referenceClosureJSON);
792-
returnserializeServerReferenceID(refId);
807+
constserverReferenceId=serializeServerReferenceID(refId);
808+
// Store the server reference ID for deduplication.
809+
writtenObjects.set(value,serverReferenceId);
810+
returnserverReferenceId;
793811
}
794812
if (temporaryReferences !== undefined &&key.indexOf(':')===-1){
795813
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.

‎packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,17 @@ function prerenderToNodeStream(
332332
functiondecodeReplyFromBusboy<T>(
333333
busboyStream: Busboy,
334334
moduleBasePath: ServerManifest,
335-
options?: {temporaryReferences?: TemporaryReferenceSet},
335+
options?: {
336+
temporaryReferences?: TemporaryReferenceSet,
337+
arraySizeLimit?: number,
338+
},
336339
): Thenable<T>{
337340
const response =createResponse(
338341
moduleBasePath,
339342
'',
340343
options ? options.temporaryReferences : undefined,
344+
undefined,
345+
options ? options.arraySizeLimit : undefined,
341346
);
342347
letpendingFiles=0;
343348
constqueuedFields: Array<string>=[];
@@ -403,7 +408,10 @@ function decodeReplyFromBusboy<T>(
403408
functiondecodeReply<T>(
404409
body: string|FormData,
405410
moduleBasePath: ServerManifest,
406-
options?: {temporaryReferences?: TemporaryReferenceSet},
411+
options?: {
412+
temporaryReferences?: TemporaryReferenceSet,
413+
arraySizeLimit?: number,
414+
},
407415
): Thenable<T>{
408416
if(typeofbody=== 'string'){
409417
constform=newFormData();
@@ -415,6 +423,7 @@ function decodeReply<T>(
415423
'',
416424
options ? options.temporaryReferences : undefined,
417425
body,
426+
options ? options.arraySizeLimit : undefined,
418427
);
419428
constroot=getRoot<T>(response);
420429
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ export function registerServerActions(manifest: ServerManifest) {
248248

249249
exportfunctiondecodeReply<T>(
250250
body: string|FormData,
251-
options?: {temporaryReferences?: TemporaryReferenceSet},
251+
options?: {
252+
temporaryReferences?: TemporaryReferenceSet,
253+
arraySizeLimit?: number,
254+
},
252255
): Thenable<T>{
253256
if(typeofbody==='string'){
254257
constform=newFormData();
@@ -260,6 +263,7 @@ export function decodeReply<T>(
260263
'',
261264
options ? options.temporaryReferences : undefined,
262265
body,
266+
options ? options.arraySizeLimit : undefined,
263267
);
264268
constroot=getRoot<T>(response);
265269
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ export function registerServerActions(manifest: ServerManifest) {
253253

254254
exportfunctiondecodeReply<T>(
255255
body: string|FormData,
256-
options?: {temporaryReferences?: TemporaryReferenceSet},
256+
options?: {
257+
temporaryReferences?: TemporaryReferenceSet,
258+
arraySizeLimit?: number,
259+
},
257260
): Thenable<T>{
258261
if(typeofbody==='string'){
259262
constform=newFormData();
@@ -265,6 +268,7 @@ export function decodeReply<T>(
265268
'',
266269
options ? options.temporaryReferences : undefined,
267270
body,
271+
options ? options.arraySizeLimit : undefined,
268272
);
269273
constroot=getRoot<T>(response);
270274
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,17 @@ export function registerServerActions(manifest: ServerManifest) {
562562

563563
exportfunctiondecodeReplyFromBusboy<T>(
564564
busboyStream: Busboy,
565-
options?: {temporaryReferences?: TemporaryReferenceSet},
565+
options?: {
566+
temporaryReferences?: TemporaryReferenceSet,
567+
arraySizeLimit?: number,
568+
},
566569
): Thenable<T>{
567570
const response =createResponse(
568571
serverManifest,
569572
'',
570573
options ? options.temporaryReferences : undefined,
574+
undefined,
575+
options ? options.arraySizeLimit : undefined,
571576
);
572577
letpendingFiles=0;
573578
constqueuedFields: Array<string>=[];
@@ -632,7 +637,10 @@ export function decodeReplyFromBusboy<T>(
632637

633638
exportfunctiondecodeReply<T>(
634639
body: string|FormData,
635-
options?: {temporaryReferences?: TemporaryReferenceSet},
640+
options?: {
641+
temporaryReferences?: TemporaryReferenceSet,
642+
arraySizeLimit?: number,
643+
},
636644
): Thenable<T>{
637645
if(typeofbody=== 'string'){
638646
constform=newFormData();
@@ -644,6 +652,7 @@ export function decodeReply<T>(
644652
'',
645653
options ? options.temporaryReferences : undefined,
646654
body,
655+
options ? options.arraySizeLimit : undefined,
647656
);
648657
constroot=getRoot<T>(response);
649658
close(response);
@@ -652,7 +661,10 @@ export function decodeReply<T>(
652661

653662
exportfunctiondecodeReplyFromAsyncIterable<T>(
654663
iterable: AsyncIterable<[string,string|File]>,
655-
options?: {temporaryReferences?: TemporaryReferenceSet},
664+
options?: {
665+
temporaryReferences?: TemporaryReferenceSet,
666+
arraySizeLimit?: number,
667+
},
656668
): Thenable<T>{
657669
constiterator: AsyncIterator<[string,string|File]>=
658670
iterable[ASYNC_ITERATOR]();
@@ -661,6 +673,8 @@ export function decodeReplyFromAsyncIterable<T>(
661673
serverManifest,
662674
'',
663675
options ? options.temporaryReferences : undefined,
676+
undefined,
677+
options ? options.arraySizeLimit : undefined,
664678
);
665679

666680
functionprogress(

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ function prerender(
242242
functiondecodeReply<T>(
243243
body: string|FormData,
244244
turbopackMap: ServerManifest,
245-
options?: {temporaryReferences?: TemporaryReferenceSet},
245+
options?: {
246+
temporaryReferences?: TemporaryReferenceSet,
247+
arraySizeLimit?: number,
248+
},
246249
): Thenable<T>{
247250
if(typeofbody==='string'){
248251
constform=newFormData();
@@ -254,6 +257,7 @@ function decodeReply<T>(
254257
'',
255258
options ? options.temporaryReferences : undefined,
256259
body,
260+
options ? options.arraySizeLimit : undefined,
257261
);
258262
constroot=getRoot<T>(response);
259263
close(response);

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ function prerender(
247247
functiondecodeReply<T>(
248248
body: string|FormData,
249249
turbopackMap: ServerManifest,
250-
options?: {temporaryReferences?: TemporaryReferenceSet},
250+
options?: {
251+
temporaryReferences?: TemporaryReferenceSet,
252+
arraySizeLimit?: number,
253+
},
251254
): Thenable<T>{
252255
if(typeofbody==='string'){
253256
constform=newFormData();
@@ -259,6 +262,7 @@ function decodeReply<T>(
259262
'',
260263
options ? options.temporaryReferences : undefined,
261264
body,
265+
options ? options.arraySizeLimit : undefined,
262266
);
263267
constroot=getRoot<T>(response);
264268
close(response);
@@ -268,7 +272,10 @@ function decodeReply<T>(
268272
functiondecodeReplyFromAsyncIterable<T>(
269273
iterable: AsyncIterable<[string,string|File]>,
270274
turbopackMap: ServerManifest,
271-
options?: {temporaryReferences?: TemporaryReferenceSet},
275+
options?: {
276+
temporaryReferences?: TemporaryReferenceSet,
277+
arraySizeLimit?: number,
278+
},
272279
): Thenable<T>{
273280
constiterator: AsyncIterator<[string,string|File]>=
274281
iterable[ASYNC_ITERATOR]();
@@ -277,6 +284,8 @@ function decodeReplyFromAsyncIterable<T>(
277284
turbopackMap,
278285
'',
279286
options ? options.temporaryReferences : undefined,
287+
undefined,
288+
options ? options.arraySizeLimit : undefined,
280289
);
281290

282291
functionprogress(

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 63d61c7

Browse files
sebmarkbagegnofflubieowoceunstubbable
committed
Add more DoS mitigations to React Flight Reply, and harden React Flight
Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
1 parent 612e371 commit 63d61c7

17 files changed

Lines changed: 853 additions & 267 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
9898

9999
import{getOwnerStackByComponentInfoInDev}from'shared/ReactComponentInfoStack';
100100

101+
importhasOwnPropertyfrom'shared/hasOwnProperty';
102+
101103
import{injectInternals}from'./ReactFlightClientDevToolsHook';
102104

103105
import{OMITTED_PROP_ERROR}from'shared/ReactFlightPropertyAccess';
@@ -163,6 +165,8 @@ const INITIALIZED = 'fulfilled';
163165
constERRORED='rejected';
164166
constHALTED='halted';// DEV-only. Means it never resolves even if connection closes.
165167

168+
const__PROTO__='__proto__';
169+
166170
typePendingChunk<T>={
167171
status: 'pending',
168172
value: null|Array<InitializationReference|(T=>mixed)>,
@@ -1496,7 +1500,16 @@ function fulfillReference(
14961500
}
14971501
}
14981502
}
1499-
value=value[path[i]];
1503+
constname=path[i];
1504+
if(
1505+
typeofvalue=== 'object' &&
1506+
value!==null&&
1507+
hasOwnProperty.call(value,name)
1508+
){
1509+
value=value[name];
1510+
} else {
1511+
thrownewError('Invalid reference.');
1512+
}
15001513
}
15011514

15021515
while(
@@ -1532,7 +1545,9 @@ function fulfillReference(
15321545
}
15331546

15341547
constmappedValue=map(response,value,parentObject,key);
1535-
parentObject[key]=mappedValue;
1548+
if(key!==__PROTO__){
1549+
parentObject[key]=mappedValue;
1550+
}
15361551

15371552
// If this is the root object for a model reference, where `handler.value`
15381553
// is a stale `null`, the resolved value can be used directly.
@@ -1799,7 +1814,9 @@ function loadServerReference<A: Iterable<any>, T>(
17991814
response._encodeFormAction,
18001815
);
18011816

1802-
parentObject[key]=resolvedValue;
1817+
if(key!==__PROTO__){
1818+
parentObject[key]=resolvedValue;
1819+
}
18031820

18041821
// If this is the root object for a model reference, where `handler.value`
18051822
// is a stale `null`, the resolved value can be used directly.
@@ -2177,29 +2194,31 @@ function defineLazyGetter<T>(
21772194
): any {
21782195
// We don't immediately initialize it even if it's resolved.
21792196
// Instead, we wait for the getter to get accessed.
2180-
Object.defineProperty(parentObject,key,{
2181-
get: function(){
2182-
if(chunk.status===RESOLVED_MODEL){
2183-
// If it was now resolved, then we initialize it. This may then discover
2184-
// a new set of lazy references that are then asked for eagerly in case
2185-
// we get that deep.
2186-
initializeModelChunk(chunk);
2187-
}
2188-
switch(chunk.status){
2189-
caseINITIALIZED: {
2190-
returnchunk.value;
2197+
if(key!==__PROTO__){
2198+
Object.defineProperty(parentObject,key,{
2199+
get: function(){
2200+
if(chunk.status===RESOLVED_MODEL){
2201+
// If it was now resolved, then we initialize it. This may then discover
2202+
// a new set of lazy references that are then asked for eagerly in case
2203+
// we get that deep.
2204+
initializeModelChunk(chunk);
21912205
}
2192-
caseERRORED:
2193-
throwchunk.reason;
2194-
}
2195-
// Otherwise, we didn't have enough time to load the object before it was
2196-
// accessed or the connection closed. So we just log that it was omitted.
2197-
// TODO: We should ideally throw here to indicate a difference.
2198-
returnOMITTED_PROP_ERROR;
2199-
},
2200-
enumerable: true,
2201-
configurable: false,
2202-
});
2206+
switch(chunk.status){
2207+
caseINITIALIZED: {
2208+
returnchunk.value;
2209+
}
2210+
caseERRORED:
2211+
throwchunk.reason;
2212+
}
2213+
// Otherwise, we didn't have enough time to load the object before it was
2214+
// accessed or the connection closed. So we just log that it was omitted.
2215+
// TODO: We should ideally throw here to indicate a difference.
2216+
returnOMITTED_PROP_ERROR;
2217+
},
2218+
enumerable: true,
2219+
configurable: false,
2220+
});
2221+
}
22032222
return null;
22042223
}
22052224

@@ -2510,14 +2529,16 @@ function parseModelString(
25102529
// In DEV mode we encode omitted objects in logs as a getter that throws
25112530
// so that when you try to access it on the client, you know why that
25122531
// happened.
2513-
Object.defineProperty(parentObject,key,{
2514-
get: function(){
2515-
// TODO: We should ideally throw here to indicate a difference.
2516-
returnOMITTED_PROP_ERROR;
2517-
},
2518-
enumerable: true,
2519-
configurable: false,
2520-
});
2532+
if(key!==__PROTO__){
2533+
Object.defineProperty(parentObject,key,{
2534+
get: function(){
2535+
// TODO: We should ideally throw here to indicate a difference.
2536+
returnOMITTED_PROP_ERROR;
2537+
},
2538+
enumerable: true,
2539+
configurable: false,
2540+
});
2541+
}
25212542
returnnull;
25222543
}
25232544
// Fallthrough
@@ -5143,6 +5164,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
51435164
function createFromJSONCallback(response: Response) {
51445165
// $FlowFixMe[missing-this-annot]
51455166
returnfunction(key: string,value: JSONValue){
5167+
if(key===__PROTO__){
5168+
returnundefined;
5169+
}
51465170
if(typeofvalue==='string'){
51475171
// We can't use .bind here because we need the "this" value.
51485172
returnparseModelString(response,this,key,value);

‎packages/react-client/src/ReactFlightReplyClient.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export type ReactServerValue =
9595

9696
type ReactServerObject = {+[key: string]: ReactServerValue};
9797

98+
const __PROTO__ = '__proto__';
99+
98100
function serializeByValueID(id: number): string {
99101
return'$'+id.toString(16);
100102
}
@@ -361,6 +363,15 @@ export function processReply(
361363
): ReactJSONValue {
362364
constparent=this;
363365

366+
if(__DEV__){
367+
if(key===__PROTO__){
368+
console.error(
369+
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
370+
describeObjectForErrorMessage(parent,key),
371+
);
372+
}
373+
}
374+
364375
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
365376
if(__DEV__){
366377
// $FlowFixMe[incompatible-use]
@@ -780,6 +791,10 @@ export function processReply(
780791
if (typeof value === 'function') {
781792
constreferenceClosure=knownServerReferences.get(value);
782793
if(referenceClosure!==undefined){
794+
const existingReference =writtenObjects.get(value);
795+
if(existingReference!==undefined){
796+
return existingReference;
797+
}
783798
const{id, bound}=referenceClosure;
784799
constreferenceClosureJSON=JSON.stringify({id, bound},resolveToJSON);
785800
if(formData===null){
@@ -789,7 +804,10 @@ export function processReply(
789804
// The reference to this function came from the same client so we can pass it back.
790805
constrefId=nextPartId++;
791806
formData.set(formFieldPrefix+refId,referenceClosureJSON);
792-
returnserializeServerReferenceID(refId);
807+
constserverReferenceId=serializeServerReferenceID(refId);
808+
// Store the server reference ID for deduplication.
809+
writtenObjects.set(value,serverReferenceId);
810+
returnserverReferenceId;
793811
}
794812
if (temporaryReferences !== undefined &&key.indexOf(':')===-1){
795813
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.

‎packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,17 @@ function prerenderToNodeStream(
332332
functiondecodeReplyFromBusboy<T>(
333333
busboyStream: Busboy,
334334
moduleBasePath: ServerManifest,
335-
options?: {temporaryReferences?: TemporaryReferenceSet},
335+
options?: {
336+
temporaryReferences?: TemporaryReferenceSet,
337+
arraySizeLimit?: number,
338+
},
336339
): Thenable<T>{
337340
const response =createResponse(
338341
moduleBasePath,
339342
'',
340343
options ? options.temporaryReferences : undefined,
344+
undefined,
345+
options ? options.arraySizeLimit : undefined,
341346
);
342347
letpendingFiles=0;
343348
constqueuedFields: Array<string>=[];
@@ -403,7 +408,10 @@ function decodeReplyFromBusboy<T>(
403408
functiondecodeReply<T>(
404409
body: string|FormData,
405410
moduleBasePath: ServerManifest,
406-
options?: {temporaryReferences?: TemporaryReferenceSet},
411+
options?: {
412+
temporaryReferences?: TemporaryReferenceSet,
413+
arraySizeLimit?: number,
414+
},
407415
): Thenable<T>{
408416
if(typeofbody=== 'string'){
409417
constform=newFormData();
@@ -415,6 +423,7 @@ function decodeReply<T>(
415423
'',
416424
options ? options.temporaryReferences : undefined,
417425
body,
426+
options ? options.arraySizeLimit : undefined,
418427
);
419428
constroot=getRoot<T>(response);
420429
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ export function registerServerActions(manifest: ServerManifest) {
248248

249249
exportfunctiondecodeReply<T>(
250250
body: string|FormData,
251-
options?: {temporaryReferences?: TemporaryReferenceSet},
251+
options?: {
252+
temporaryReferences?: TemporaryReferenceSet,
253+
arraySizeLimit?: number,
254+
},
252255
): Thenable<T>{
253256
if(typeofbody==='string'){
254257
constform=newFormData();
@@ -260,6 +263,7 @@ export function decodeReply<T>(
260263
'',
261264
options ? options.temporaryReferences : undefined,
262265
body,
266+
options ? options.arraySizeLimit : undefined,
263267
);
264268
constroot=getRoot<T>(response);
265269
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ export function registerServerActions(manifest: ServerManifest) {
253253

254254
exportfunctiondecodeReply<T>(
255255
body: string|FormData,
256-
options?: {temporaryReferences?: TemporaryReferenceSet},
256+
options?: {
257+
temporaryReferences?: TemporaryReferenceSet,
258+
arraySizeLimit?: number,
259+
},
257260
): Thenable<T>{
258261
if(typeofbody==='string'){
259262
constform=newFormData();
@@ -265,6 +268,7 @@ export function decodeReply<T>(
265268
'',
266269
options ? options.temporaryReferences : undefined,
267270
body,
271+
options ? options.arraySizeLimit : undefined,
268272
);
269273
constroot=getRoot<T>(response);
270274
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,17 @@ export function registerServerActions(manifest: ServerManifest) {
562562

563563
exportfunctiondecodeReplyFromBusboy<T>(
564564
busboyStream: Busboy,
565-
options?: {temporaryReferences?: TemporaryReferenceSet},
565+
options?: {
566+
temporaryReferences?: TemporaryReferenceSet,
567+
arraySizeLimit?: number,
568+
},
566569
): Thenable<T>{
567570
const response =createResponse(
568571
serverManifest,
569572
'',
570573
options ? options.temporaryReferences : undefined,
574+
undefined,
575+
options ? options.arraySizeLimit : undefined,
571576
);
572577
letpendingFiles=0;
573578
constqueuedFields: Array<string>=[];
@@ -632,7 +637,10 @@ export function decodeReplyFromBusboy<T>(
632637

633638
exportfunctiondecodeReply<T>(
634639
body: string|FormData,
635-
options?: {temporaryReferences?: TemporaryReferenceSet},
640+
options?: {
641+
temporaryReferences?: TemporaryReferenceSet,
642+
arraySizeLimit?: number,
643+
},
636644
): Thenable<T>{
637645
if(typeofbody=== 'string'){
638646
constform=newFormData();
@@ -644,6 +652,7 @@ export function decodeReply<T>(
644652
'',
645653
options ? options.temporaryReferences : undefined,
646654
body,
655+
options ? options.arraySizeLimit : undefined,
647656
);
648657
constroot=getRoot<T>(response);
649658
close(response);
@@ -652,7 +661,10 @@ export function decodeReply<T>(
652661

653662
exportfunctiondecodeReplyFromAsyncIterable<T>(
654663
iterable: AsyncIterable<[string,string|File]>,
655-
options?: {temporaryReferences?: TemporaryReferenceSet},
664+
options?: {
665+
temporaryReferences?: TemporaryReferenceSet,
666+
arraySizeLimit?: number,
667+
},
656668
): Thenable<T>{
657669
constiterator: AsyncIterator<[string,string|File]>=
658670
iterable[ASYNC_ITERATOR]();
@@ -661,6 +673,8 @@ export function decodeReplyFromAsyncIterable<T>(
661673
serverManifest,
662674
'',
663675
options ? options.temporaryReferences : undefined,
676+
undefined,
677+
options ? options.arraySizeLimit : undefined,
664678
);
665679

666680
functionprogress(

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ function prerender(
242242
functiondecodeReply<T>(
243243
body: string|FormData,
244244
turbopackMap: ServerManifest,
245-
options?: {temporaryReferences?: TemporaryReferenceSet},
245+
options?: {
246+
temporaryReferences?: TemporaryReferenceSet,
247+
arraySizeLimit?: number,
248+
},
246249
): Thenable<T>{
247250
if(typeofbody==='string'){
248251
constform=newFormData();
@@ -254,6 +257,7 @@ function decodeReply<T>(
254257
'',
255258
options ? options.temporaryReferences : undefined,
256259
body,
260+
options ? options.arraySizeLimit : undefined,
257261
);
258262
constroot=getRoot<T>(response);
259263
close(response);

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ function prerender(
247247
functiondecodeReply<T>(
248248
body: string|FormData,
249249
turbopackMap: ServerManifest,
250-
options?: {temporaryReferences?: TemporaryReferenceSet},
250+
options?: {
251+
temporaryReferences?: TemporaryReferenceSet,
252+
arraySizeLimit?: number,
253+
},
251254
): Thenable<T>{
252255
if(typeofbody==='string'){
253256
constform=newFormData();
@@ -259,6 +262,7 @@ function decodeReply<T>(
259262
'',
260263
options ? options.temporaryReferences : undefined,
261264
body,
265+
options ? options.arraySizeLimit : undefined,
262266
);
263267
constroot=getRoot<T>(response);
264268
close(response);
@@ -268,7 +272,10 @@ function decodeReply<T>(
268272
functiondecodeReplyFromAsyncIterable<T>(
269273
iterable: AsyncIterable<[string,string|File]>,
270274
turbopackMap: ServerManifest,
271-
options?: {temporaryReferences?: TemporaryReferenceSet},
275+
options?: {
276+
temporaryReferences?: TemporaryReferenceSet,
277+
arraySizeLimit?: number,
278+
},
272279
): Thenable<T>{
273280
constiterator: AsyncIterator<[string,string|File]>=
274281
iterable[ASYNC_ITERATOR]();
@@ -277,6 +284,8 @@ function decodeReplyFromAsyncIterable<T>(
277284
turbopackMap,
278285
'',
279286
options ? options.temporaryReferences : undefined,
287+
undefined,
288+
options ? options.arraySizeLimit : undefined,
280289
);
281290

282291
functionprogress(

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 63d61c7

Browse files
sebmarkbagegnofflubieowoceunstubbable
committed
Add more DoS mitigations to React Flight Reply, and harden React Flight
Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
1 parent 612e371 commit 63d61c7

17 files changed

Lines changed: 853 additions & 267 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
9898

9999
import{getOwnerStackByComponentInfoInDev}from'shared/ReactComponentInfoStack';
100100

101+
importhasOwnPropertyfrom'shared/hasOwnProperty';
102+
101103
import{injectInternals}from'./ReactFlightClientDevToolsHook';
102104

103105
import{OMITTED_PROP_ERROR}from'shared/ReactFlightPropertyAccess';
@@ -163,6 +165,8 @@ const INITIALIZED = 'fulfilled';
163165
constERRORED='rejected';
164166
constHALTED='halted';// DEV-only. Means it never resolves even if connection closes.
165167

168+
const__PROTO__='__proto__';
169+
166170
typePendingChunk<T>={
167171
status: 'pending',
168172
value: null|Array<InitializationReference|(T=>mixed)>,
@@ -1496,7 +1500,16 @@ function fulfillReference(
14961500
}
14971501
}
14981502
}
1499-
value=value[path[i]];
1503+
constname=path[i];
1504+
if(
1505+
typeofvalue=== 'object' &&
1506+
value!==null&&
1507+
hasOwnProperty.call(value,name)
1508+
){
1509+
value=value[name];
1510+
} else {
1511+
thrownewError('Invalid reference.');
1512+
}
15001513
}
15011514

15021515
while(
@@ -1532,7 +1545,9 @@ function fulfillReference(
15321545
}
15331546

15341547
constmappedValue=map(response,value,parentObject,key);
1535-
parentObject[key]=mappedValue;
1548+
if(key!==__PROTO__){
1549+
parentObject[key]=mappedValue;
1550+
}
15361551

15371552
// If this is the root object for a model reference, where `handler.value`
15381553
// is a stale `null`, the resolved value can be used directly.
@@ -1799,7 +1814,9 @@ function loadServerReference<A: Iterable<any>, T>(
17991814
response._encodeFormAction,
18001815
);
18011816

1802-
parentObject[key]=resolvedValue;
1817+
if(key!==__PROTO__){
1818+
parentObject[key]=resolvedValue;
1819+
}
18031820

18041821
// If this is the root object for a model reference, where `handler.value`
18051822
// is a stale `null`, the resolved value can be used directly.
@@ -2177,29 +2194,31 @@ function defineLazyGetter<T>(
21772194
): any {
21782195
// We don't immediately initialize it even if it's resolved.
21792196
// Instead, we wait for the getter to get accessed.
2180-
Object.defineProperty(parentObject,key,{
2181-
get: function(){
2182-
if(chunk.status===RESOLVED_MODEL){
2183-
// If it was now resolved, then we initialize it. This may then discover
2184-
// a new set of lazy references that are then asked for eagerly in case
2185-
// we get that deep.
2186-
initializeModelChunk(chunk);
2187-
}
2188-
switch(chunk.status){
2189-
caseINITIALIZED: {
2190-
returnchunk.value;
2197+
if(key!==__PROTO__){
2198+
Object.defineProperty(parentObject,key,{
2199+
get: function(){
2200+
if(chunk.status===RESOLVED_MODEL){
2201+
// If it was now resolved, then we initialize it. This may then discover
2202+
// a new set of lazy references that are then asked for eagerly in case
2203+
// we get that deep.
2204+
initializeModelChunk(chunk);
21912205
}
2192-
caseERRORED:
2193-
throwchunk.reason;
2194-
}
2195-
// Otherwise, we didn't have enough time to load the object before it was
2196-
// accessed or the connection closed. So we just log that it was omitted.
2197-
// TODO: We should ideally throw here to indicate a difference.
2198-
returnOMITTED_PROP_ERROR;
2199-
},
2200-
enumerable: true,
2201-
configurable: false,
2202-
});
2206+
switch(chunk.status){
2207+
caseINITIALIZED: {
2208+
returnchunk.value;
2209+
}
2210+
caseERRORED:
2211+
throwchunk.reason;
2212+
}
2213+
// Otherwise, we didn't have enough time to load the object before it was
2214+
// accessed or the connection closed. So we just log that it was omitted.
2215+
// TODO: We should ideally throw here to indicate a difference.
2216+
returnOMITTED_PROP_ERROR;
2217+
},
2218+
enumerable: true,
2219+
configurable: false,
2220+
});
2221+
}
22032222
return null;
22042223
}
22052224

@@ -2510,14 +2529,16 @@ function parseModelString(
25102529
// In DEV mode we encode omitted objects in logs as a getter that throws
25112530
// so that when you try to access it on the client, you know why that
25122531
// happened.
2513-
Object.defineProperty(parentObject,key,{
2514-
get: function(){
2515-
// TODO: We should ideally throw here to indicate a difference.
2516-
returnOMITTED_PROP_ERROR;
2517-
},
2518-
enumerable: true,
2519-
configurable: false,
2520-
});
2532+
if(key!==__PROTO__){
2533+
Object.defineProperty(parentObject,key,{
2534+
get: function(){
2535+
// TODO: We should ideally throw here to indicate a difference.
2536+
returnOMITTED_PROP_ERROR;
2537+
},
2538+
enumerable: true,
2539+
configurable: false,
2540+
});
2541+
}
25212542
returnnull;
25222543
}
25232544
// Fallthrough
@@ -5143,6 +5164,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
51435164
function createFromJSONCallback(response: Response) {
51445165
// $FlowFixMe[missing-this-annot]
51455166
returnfunction(key: string,value: JSONValue){
5167+
if(key===__PROTO__){
5168+
returnundefined;
5169+
}
51465170
if(typeofvalue==='string'){
51475171
// We can't use .bind here because we need the "this" value.
51485172
returnparseModelString(response,this,key,value);

‎packages/react-client/src/ReactFlightReplyClient.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export type ReactServerValue =
9595

9696
type ReactServerObject = {+[key: string]: ReactServerValue};
9797

98+
const __PROTO__ = '__proto__';
99+
98100
function serializeByValueID(id: number): string {
99101
return'$'+id.toString(16);
100102
}
@@ -361,6 +363,15 @@ export function processReply(
361363
): ReactJSONValue {
362364
constparent=this;
363365

366+
if(__DEV__){
367+
if(key===__PROTO__){
368+
console.error(
369+
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
370+
describeObjectForErrorMessage(parent,key),
371+
);
372+
}
373+
}
374+
364375
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
365376
if(__DEV__){
366377
// $FlowFixMe[incompatible-use]
@@ -780,6 +791,10 @@ export function processReply(
780791
if (typeof value === 'function') {
781792
constreferenceClosure=knownServerReferences.get(value);
782793
if(referenceClosure!==undefined){
794+
const existingReference =writtenObjects.get(value);
795+
if(existingReference!==undefined){
796+
return existingReference;
797+
}
783798
const{id, bound}=referenceClosure;
784799
constreferenceClosureJSON=JSON.stringify({id, bound},resolveToJSON);
785800
if(formData===null){
@@ -789,7 +804,10 @@ export function processReply(
789804
// The reference to this function came from the same client so we can pass it back.
790805
constrefId=nextPartId++;
791806
formData.set(formFieldPrefix+refId,referenceClosureJSON);
792-
returnserializeServerReferenceID(refId);
807+
constserverReferenceId=serializeServerReferenceID(refId);
808+
// Store the server reference ID for deduplication.
809+
writtenObjects.set(value,serverReferenceId);
810+
returnserverReferenceId;
793811
}
794812
if (temporaryReferences !== undefined &&key.indexOf(':')===-1){
795813
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.

‎packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,17 @@ function prerenderToNodeStream(
332332
functiondecodeReplyFromBusboy<T>(
333333
busboyStream: Busboy,
334334
moduleBasePath: ServerManifest,
335-
options?: {temporaryReferences?: TemporaryReferenceSet},
335+
options?: {
336+
temporaryReferences?: TemporaryReferenceSet,
337+
arraySizeLimit?: number,
338+
},
336339
): Thenable<T>{
337340
const response =createResponse(
338341
moduleBasePath,
339342
'',
340343
options ? options.temporaryReferences : undefined,
344+
undefined,
345+
options ? options.arraySizeLimit : undefined,
341346
);
342347
letpendingFiles=0;
343348
constqueuedFields: Array<string>=[];
@@ -403,7 +408,10 @@ function decodeReplyFromBusboy<T>(
403408
functiondecodeReply<T>(
404409
body: string|FormData,
405410
moduleBasePath: ServerManifest,
406-
options?: {temporaryReferences?: TemporaryReferenceSet},
411+
options?: {
412+
temporaryReferences?: TemporaryReferenceSet,
413+
arraySizeLimit?: number,
414+
},
407415
): Thenable<T>{
408416
if(typeofbody=== 'string'){
409417
constform=newFormData();
@@ -415,6 +423,7 @@ function decodeReply<T>(
415423
'',
416424
options ? options.temporaryReferences : undefined,
417425
body,
426+
options ? options.arraySizeLimit : undefined,
418427
);
419428
constroot=getRoot<T>(response);
420429
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ export function registerServerActions(manifest: ServerManifest) {
248248

249249
exportfunctiondecodeReply<T>(
250250
body: string|FormData,
251-
options?: {temporaryReferences?: TemporaryReferenceSet},
251+
options?: {
252+
temporaryReferences?: TemporaryReferenceSet,
253+
arraySizeLimit?: number,
254+
},
252255
): Thenable<T>{
253256
if(typeofbody==='string'){
254257
constform=newFormData();
@@ -260,6 +263,7 @@ export function decodeReply<T>(
260263
'',
261264
options ? options.temporaryReferences : undefined,
262265
body,
266+
options ? options.arraySizeLimit : undefined,
263267
);
264268
constroot=getRoot<T>(response);
265269
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ export function registerServerActions(manifest: ServerManifest) {
253253

254254
exportfunctiondecodeReply<T>(
255255
body: string|FormData,
256-
options?: {temporaryReferences?: TemporaryReferenceSet},
256+
options?: {
257+
temporaryReferences?: TemporaryReferenceSet,
258+
arraySizeLimit?: number,
259+
},
257260
): Thenable<T>{
258261
if(typeofbody==='string'){
259262
constform=newFormData();
@@ -265,6 +268,7 @@ export function decodeReply<T>(
265268
'',
266269
options ? options.temporaryReferences : undefined,
267270
body,
271+
options ? options.arraySizeLimit : undefined,
268272
);
269273
constroot=getRoot<T>(response);
270274
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,17 @@ export function registerServerActions(manifest: ServerManifest) {
562562

563563
exportfunctiondecodeReplyFromBusboy<T>(
564564
busboyStream: Busboy,
565-
options?: {temporaryReferences?: TemporaryReferenceSet},
565+
options?: {
566+
temporaryReferences?: TemporaryReferenceSet,
567+
arraySizeLimit?: number,
568+
},
566569
): Thenable<T>{
567570
const response =createResponse(
568571
serverManifest,
569572
'',
570573
options ? options.temporaryReferences : undefined,
574+
undefined,
575+
options ? options.arraySizeLimit : undefined,
571576
);
572577
letpendingFiles=0;
573578
constqueuedFields: Array<string>=[];
@@ -632,7 +637,10 @@ export function decodeReplyFromBusboy<T>(
632637

633638
exportfunctiondecodeReply<T>(
634639
body: string|FormData,
635-
options?: {temporaryReferences?: TemporaryReferenceSet},
640+
options?: {
641+
temporaryReferences?: TemporaryReferenceSet,
642+
arraySizeLimit?: number,
643+
},
636644
): Thenable<T>{
637645
if(typeofbody=== 'string'){
638646
constform=newFormData();
@@ -644,6 +652,7 @@ export function decodeReply<T>(
644652
'',
645653
options ? options.temporaryReferences : undefined,
646654
body,
655+
options ? options.arraySizeLimit : undefined,
647656
);
648657
constroot=getRoot<T>(response);
649658
close(response);
@@ -652,7 +661,10 @@ export function decodeReply<T>(
652661

653662
exportfunctiondecodeReplyFromAsyncIterable<T>(
654663
iterable: AsyncIterable<[string,string|File]>,
655-
options?: {temporaryReferences?: TemporaryReferenceSet},
664+
options?: {
665+
temporaryReferences?: TemporaryReferenceSet,
666+
arraySizeLimit?: number,
667+
},
656668
): Thenable<T>{
657669
constiterator: AsyncIterator<[string,string|File]>=
658670
iterable[ASYNC_ITERATOR]();
@@ -661,6 +673,8 @@ export function decodeReplyFromAsyncIterable<T>(
661673
serverManifest,
662674
'',
663675
options ? options.temporaryReferences : undefined,
676+
undefined,
677+
options ? options.arraySizeLimit : undefined,
664678
);
665679

666680
functionprogress(

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ function prerender(
242242
functiondecodeReply<T>(
243243
body: string|FormData,
244244
turbopackMap: ServerManifest,
245-
options?: {temporaryReferences?: TemporaryReferenceSet},
245+
options?: {
246+
temporaryReferences?: TemporaryReferenceSet,
247+
arraySizeLimit?: number,
248+
},
246249
): Thenable<T>{
247250
if(typeofbody==='string'){
248251
constform=newFormData();
@@ -254,6 +257,7 @@ function decodeReply<T>(
254257
'',
255258
options ? options.temporaryReferences : undefined,
256259
body,
260+
options ? options.arraySizeLimit : undefined,
257261
);
258262
constroot=getRoot<T>(response);
259263
close(response);

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ function prerender(
247247
functiondecodeReply<T>(
248248
body: string|FormData,
249249
turbopackMap: ServerManifest,
250-
options?: {temporaryReferences?: TemporaryReferenceSet},
250+
options?: {
251+
temporaryReferences?: TemporaryReferenceSet,
252+
arraySizeLimit?: number,
253+
},
251254
): Thenable<T>{
252255
if(typeofbody==='string'){
253256
constform=newFormData();
@@ -259,6 +262,7 @@ function decodeReply<T>(
259262
'',
260263
options ? options.temporaryReferences : undefined,
261264
body,
265+
options ? options.arraySizeLimit : undefined,
262266
);
263267
constroot=getRoot<T>(response);
264268
close(response);
@@ -268,7 +272,10 @@ function decodeReply<T>(
268272
functiondecodeReplyFromAsyncIterable<T>(
269273
iterable: AsyncIterable<[string,string|File]>,
270274
turbopackMap: ServerManifest,
271-
options?: {temporaryReferences?: TemporaryReferenceSet},
275+
options?: {
276+
temporaryReferences?: TemporaryReferenceSet,
277+
arraySizeLimit?: number,
278+
},
272279
): Thenable<T>{
273280
constiterator: AsyncIterator<[string,string|File]>=
274281
iterable[ASYNC_ITERATOR]();
@@ -277,6 +284,8 @@ function decodeReplyFromAsyncIterable<T>(
277284
turbopackMap,
278285
'',
279286
options ? options.temporaryReferences : undefined,
287+
undefined,
288+
options ? options.arraySizeLimit : undefined,
280289
);
281290

282291
functionprogress(

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit 63d61c7

Browse files
sebmarkbagegnofflubieowoceunstubbable
committed
Add more DoS mitigations to React Flight Reply, and harden React Flight
Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
1 parent 612e371 commit 63d61c7

17 files changed

Lines changed: 853 additions & 267 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
9898

9999
import{getOwnerStackByComponentInfoInDev}from'shared/ReactComponentInfoStack';
100100

101+
importhasOwnPropertyfrom'shared/hasOwnProperty';
102+
101103
import{injectInternals}from'./ReactFlightClientDevToolsHook';
102104

103105
import{OMITTED_PROP_ERROR}from'shared/ReactFlightPropertyAccess';
@@ -163,6 +165,8 @@ const INITIALIZED = 'fulfilled';
163165
constERRORED='rejected';
164166
constHALTED='halted';// DEV-only. Means it never resolves even if connection closes.
165167

168+
const__PROTO__='__proto__';
169+
166170
typePendingChunk<T>={
167171
status: 'pending',
168172
value: null|Array<InitializationReference|(T=>mixed)>,
@@ -1496,7 +1500,16 @@ function fulfillReference(
14961500
}
14971501
}
14981502
}
1499-
value=value[path[i]];
1503+
constname=path[i];
1504+
if(
1505+
typeofvalue=== 'object' &&
1506+
value!==null&&
1507+
hasOwnProperty.call(value,name)
1508+
){
1509+
value=value[name];
1510+
} else {
1511+
thrownewError('Invalid reference.');
1512+
}
15001513
}
15011514

15021515
while(
@@ -1532,7 +1545,9 @@ function fulfillReference(
15321545
}
15331546

15341547
constmappedValue=map(response,value,parentObject,key);
1535-
parentObject[key]=mappedValue;
1548+
if(key!==__PROTO__){
1549+
parentObject[key]=mappedValue;
1550+
}
15361551

15371552
// If this is the root object for a model reference, where `handler.value`
15381553
// is a stale `null`, the resolved value can be used directly.
@@ -1799,7 +1814,9 @@ function loadServerReference<A: Iterable<any>, T>(
17991814
response._encodeFormAction,
18001815
);
18011816

1802-
parentObject[key]=resolvedValue;
1817+
if(key!==__PROTO__){
1818+
parentObject[key]=resolvedValue;
1819+
}
18031820

18041821
// If this is the root object for a model reference, where `handler.value`
18051822
// is a stale `null`, the resolved value can be used directly.
@@ -2177,29 +2194,31 @@ function defineLazyGetter<T>(
21772194
): any {
21782195
// We don't immediately initialize it even if it's resolved.
21792196
// Instead, we wait for the getter to get accessed.
2180-
Object.defineProperty(parentObject,key,{
2181-
get: function(){
2182-
if(chunk.status===RESOLVED_MODEL){
2183-
// If it was now resolved, then we initialize it. This may then discover
2184-
// a new set of lazy references that are then asked for eagerly in case
2185-
// we get that deep.
2186-
initializeModelChunk(chunk);
2187-
}
2188-
switch(chunk.status){
2189-
caseINITIALIZED: {
2190-
returnchunk.value;
2197+
if(key!==__PROTO__){
2198+
Object.defineProperty(parentObject,key,{
2199+
get: function(){
2200+
if(chunk.status===RESOLVED_MODEL){
2201+
// If it was now resolved, then we initialize it. This may then discover
2202+
// a new set of lazy references that are then asked for eagerly in case
2203+
// we get that deep.
2204+
initializeModelChunk(chunk);
21912205
}
2192-
caseERRORED:
2193-
throwchunk.reason;
2194-
}
2195-
// Otherwise, we didn't have enough time to load the object before it was
2196-
// accessed or the connection closed. So we just log that it was omitted.
2197-
// TODO: We should ideally throw here to indicate a difference.
2198-
returnOMITTED_PROP_ERROR;
2199-
},
2200-
enumerable: true,
2201-
configurable: false,
2202-
});
2206+
switch(chunk.status){
2207+
caseINITIALIZED: {
2208+
returnchunk.value;
2209+
}
2210+
caseERRORED:
2211+
throwchunk.reason;
2212+
}
2213+
// Otherwise, we didn't have enough time to load the object before it was
2214+
// accessed or the connection closed. So we just log that it was omitted.
2215+
// TODO: We should ideally throw here to indicate a difference.
2216+
returnOMITTED_PROP_ERROR;
2217+
},
2218+
enumerable: true,
2219+
configurable: false,
2220+
});
2221+
}
22032222
return null;
22042223
}
22052224

@@ -2510,14 +2529,16 @@ function parseModelString(
25102529
// In DEV mode we encode omitted objects in logs as a getter that throws
25112530
// so that when you try to access it on the client, you know why that
25122531
// happened.
2513-
Object.defineProperty(parentObject,key,{
2514-
get: function(){
2515-
// TODO: We should ideally throw here to indicate a difference.
2516-
returnOMITTED_PROP_ERROR;
2517-
},
2518-
enumerable: true,
2519-
configurable: false,
2520-
});
2532+
if(key!==__PROTO__){
2533+
Object.defineProperty(parentObject,key,{
2534+
get: function(){
2535+
// TODO: We should ideally throw here to indicate a difference.
2536+
returnOMITTED_PROP_ERROR;
2537+
},
2538+
enumerable: true,
2539+
configurable: false,
2540+
});
2541+
}
25212542
returnnull;
25222543
}
25232544
// Fallthrough
@@ -5143,6 +5164,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
51435164
function createFromJSONCallback(response: Response) {
51445165
// $FlowFixMe[missing-this-annot]
51455166
returnfunction(key: string,value: JSONValue){
5167+
if(key===__PROTO__){
5168+
returnundefined;
5169+
}
51465170
if(typeofvalue==='string'){
51475171
// We can't use .bind here because we need the "this" value.
51485172
returnparseModelString(response,this,key,value);

‎packages/react-client/src/ReactFlightReplyClient.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export type ReactServerValue =
9595

9696
type ReactServerObject = {+[key: string]: ReactServerValue};
9797

98+
const __PROTO__ = '__proto__';
99+
98100
function serializeByValueID(id: number): string {
99101
return'$'+id.toString(16);
100102
}
@@ -361,6 +363,15 @@ export function processReply(
361363
): ReactJSONValue {
362364
constparent=this;
363365

366+
if(__DEV__){
367+
if(key===__PROTO__){
368+
console.error(
369+
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
370+
describeObjectForErrorMessage(parent,key),
371+
);
372+
}
373+
}
374+
364375
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
365376
if(__DEV__){
366377
// $FlowFixMe[incompatible-use]
@@ -780,6 +791,10 @@ export function processReply(
780791
if (typeof value === 'function') {
781792
constreferenceClosure=knownServerReferences.get(value);
782793
if(referenceClosure!==undefined){
794+
const existingReference =writtenObjects.get(value);
795+
if(existingReference!==undefined){
796+
return existingReference;
797+
}
783798
const{id, bound}=referenceClosure;
784799
constreferenceClosureJSON=JSON.stringify({id, bound},resolveToJSON);
785800
if(formData===null){
@@ -789,7 +804,10 @@ export function processReply(
789804
// The reference to this function came from the same client so we can pass it back.
790805
constrefId=nextPartId++;
791806
formData.set(formFieldPrefix+refId,referenceClosureJSON);
792-
returnserializeServerReferenceID(refId);
807+
constserverReferenceId=serializeServerReferenceID(refId);
808+
// Store the server reference ID for deduplication.
809+
writtenObjects.set(value,serverReferenceId);
810+
returnserverReferenceId;
793811
}
794812
if (temporaryReferences !== undefined &&key.indexOf(':')===-1){
795813
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.

‎packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,17 @@ function prerenderToNodeStream(
332332
functiondecodeReplyFromBusboy<T>(
333333
busboyStream: Busboy,
334334
moduleBasePath: ServerManifest,
335-
options?: {temporaryReferences?: TemporaryReferenceSet},
335+
options?: {
336+
temporaryReferences?: TemporaryReferenceSet,
337+
arraySizeLimit?: number,
338+
},
336339
): Thenable<T>{
337340
const response =createResponse(
338341
moduleBasePath,
339342
'',
340343
options ? options.temporaryReferences : undefined,
344+
undefined,
345+
options ? options.arraySizeLimit : undefined,
341346
);
342347
letpendingFiles=0;
343348
constqueuedFields: Array<string>=[];
@@ -403,7 +408,10 @@ function decodeReplyFromBusboy<T>(
403408
functiondecodeReply<T>(
404409
body: string|FormData,
405410
moduleBasePath: ServerManifest,
406-
options?: {temporaryReferences?: TemporaryReferenceSet},
411+
options?: {
412+
temporaryReferences?: TemporaryReferenceSet,
413+
arraySizeLimit?: number,
414+
},
407415
): Thenable<T>{
408416
if(typeofbody=== 'string'){
409417
constform=newFormData();
@@ -415,6 +423,7 @@ function decodeReply<T>(
415423
'',
416424
options ? options.temporaryReferences : undefined,
417425
body,
426+
options ? options.arraySizeLimit : undefined,
418427
);
419428
constroot=getRoot<T>(response);
420429
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ export function registerServerActions(manifest: ServerManifest) {
248248

249249
exportfunctiondecodeReply<T>(
250250
body: string|FormData,
251-
options?: {temporaryReferences?: TemporaryReferenceSet},
251+
options?: {
252+
temporaryReferences?: TemporaryReferenceSet,
253+
arraySizeLimit?: number,
254+
},
252255
): Thenable<T>{
253256
if(typeofbody==='string'){
254257
constform=newFormData();
@@ -260,6 +263,7 @@ export function decodeReply<T>(
260263
'',
261264
options ? options.temporaryReferences : undefined,
262265
body,
266+
options ? options.arraySizeLimit : undefined,
263267
);
264268
constroot=getRoot<T>(response);
265269
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ export function registerServerActions(manifest: ServerManifest) {
253253

254254
exportfunctiondecodeReply<T>(
255255
body: string|FormData,
256-
options?: {temporaryReferences?: TemporaryReferenceSet},
256+
options?: {
257+
temporaryReferences?: TemporaryReferenceSet,
258+
arraySizeLimit?: number,
259+
},
257260
): Thenable<T>{
258261
if(typeofbody==='string'){
259262
constform=newFormData();
@@ -265,6 +268,7 @@ export function decodeReply<T>(
265268
'',
266269
options ? options.temporaryReferences : undefined,
267270
body,
271+
options ? options.arraySizeLimit : undefined,
268272
);
269273
constroot=getRoot<T>(response);
270274
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,17 @@ export function registerServerActions(manifest: ServerManifest) {
562562

563563
exportfunctiondecodeReplyFromBusboy<T>(
564564
busboyStream: Busboy,
565-
options?: {temporaryReferences?: TemporaryReferenceSet},
565+
options?: {
566+
temporaryReferences?: TemporaryReferenceSet,
567+
arraySizeLimit?: number,
568+
},
566569
): Thenable<T>{
567570
const response =createResponse(
568571
serverManifest,
569572
'',
570573
options ? options.temporaryReferences : undefined,
574+
undefined,
575+
options ? options.arraySizeLimit : undefined,
571576
);
572577
letpendingFiles=0;
573578
constqueuedFields: Array<string>=[];
@@ -632,7 +637,10 @@ export function decodeReplyFromBusboy<T>(
632637

633638
exportfunctiondecodeReply<T>(
634639
body: string|FormData,
635-
options?: {temporaryReferences?: TemporaryReferenceSet},
640+
options?: {
641+
temporaryReferences?: TemporaryReferenceSet,
642+
arraySizeLimit?: number,
643+
},
636644
): Thenable<T>{
637645
if(typeofbody=== 'string'){
638646
constform=newFormData();
@@ -644,6 +652,7 @@ export function decodeReply<T>(
644652
'',
645653
options ? options.temporaryReferences : undefined,
646654
body,
655+
options ? options.arraySizeLimit : undefined,
647656
);
648657
constroot=getRoot<T>(response);
649658
close(response);
@@ -652,7 +661,10 @@ export function decodeReply<T>(
652661

653662
exportfunctiondecodeReplyFromAsyncIterable<T>(
654663
iterable: AsyncIterable<[string,string|File]>,
655-
options?: {temporaryReferences?: TemporaryReferenceSet},
664+
options?: {
665+
temporaryReferences?: TemporaryReferenceSet,
666+
arraySizeLimit?: number,
667+
},
656668
): Thenable<T>{
657669
constiterator: AsyncIterator<[string,string|File]>=
658670
iterable[ASYNC_ITERATOR]();
@@ -661,6 +673,8 @@ export function decodeReplyFromAsyncIterable<T>(
661673
serverManifest,
662674
'',
663675
options ? options.temporaryReferences : undefined,
676+
undefined,
677+
options ? options.arraySizeLimit : undefined,
664678
);
665679

666680
functionprogress(

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ function prerender(
242242
functiondecodeReply<T>(
243243
body: string|FormData,
244244
turbopackMap: ServerManifest,
245-
options?: {temporaryReferences?: TemporaryReferenceSet},
245+
options?: {
246+
temporaryReferences?: TemporaryReferenceSet,
247+
arraySizeLimit?: number,
248+
},
246249
): Thenable<T>{
247250
if(typeofbody==='string'){
248251
constform=newFormData();
@@ -254,6 +257,7 @@ function decodeReply<T>(
254257
'',
255258
options ? options.temporaryReferences : undefined,
256259
body,
260+
options ? options.arraySizeLimit : undefined,
257261
);
258262
constroot=getRoot<T>(response);
259263
close(response);

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ function prerender(
247247
functiondecodeReply<T>(
248248
body: string|FormData,
249249
turbopackMap: ServerManifest,
250-
options?: {temporaryReferences?: TemporaryReferenceSet},
250+
options?: {
251+
temporaryReferences?: TemporaryReferenceSet,
252+
arraySizeLimit?: number,
253+
},
251254
): Thenable<T>{
252255
if(typeofbody==='string'){
253256
constform=newFormData();
@@ -259,6 +262,7 @@ function decodeReply<T>(
259262
'',
260263
options ? options.temporaryReferences : undefined,
261264
body,
265+
options ? options.arraySizeLimit : undefined,
262266
);
263267
constroot=getRoot<T>(response);
264268
close(response);
@@ -268,7 +272,10 @@ function decodeReply<T>(
268272
functiondecodeReplyFromAsyncIterable<T>(
269273
iterable: AsyncIterable<[string,string|File]>,
270274
turbopackMap: ServerManifest,
271-
options?: {temporaryReferences?: TemporaryReferenceSet},
275+
options?: {
276+
temporaryReferences?: TemporaryReferenceSet,
277+
arraySizeLimit?: number,
278+
},
272279
): Thenable<T>{
273280
constiterator: AsyncIterator<[string,string|File]>=
274281
iterable[ASYNC_ITERATOR]();
@@ -277,6 +284,8 @@ function decodeReplyFromAsyncIterable<T>(
277284
turbopackMap,
278285
'',
279286
options ? options.temporaryReferences : undefined,
287+
undefined,
288+
options ? options.arraySizeLimit : undefined,
280289
);
281290

282291
functionprogress(

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 63d61c7

Browse files
sebmarkbagegnofflubieowoceunstubbable
committed
Add more DoS mitigations to React Flight Reply, and harden React Flight
Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
1 parent 612e371 commit 63d61c7

17 files changed

Lines changed: 853 additions & 267 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
9898

9999
import{getOwnerStackByComponentInfoInDev}from'shared/ReactComponentInfoStack';
100100

101+
importhasOwnPropertyfrom'shared/hasOwnProperty';
102+
101103
import{injectInternals}from'./ReactFlightClientDevToolsHook';
102104

103105
import{OMITTED_PROP_ERROR}from'shared/ReactFlightPropertyAccess';
@@ -163,6 +165,8 @@ const INITIALIZED = 'fulfilled';
163165
constERRORED='rejected';
164166
constHALTED='halted';// DEV-only. Means it never resolves even if connection closes.
165167

168+
const__PROTO__='__proto__';
169+
166170
typePendingChunk<T>={
167171
status: 'pending',
168172
value: null|Array<InitializationReference|(T=>mixed)>,
@@ -1496,7 +1500,16 @@ function fulfillReference(
14961500
}
14971501
}
14981502
}
1499-
value=value[path[i]];
1503+
constname=path[i];
1504+
if(
1505+
typeofvalue=== 'object' &&
1506+
value!==null&&
1507+
hasOwnProperty.call(value,name)
1508+
){
1509+
value=value[name];
1510+
} else {
1511+
thrownewError('Invalid reference.');
1512+
}
15001513
}
15011514

15021515
while(
@@ -1532,7 +1545,9 @@ function fulfillReference(
15321545
}
15331546

15341547
constmappedValue=map(response,value,parentObject,key);
1535-
parentObject[key]=mappedValue;
1548+
if(key!==__PROTO__){
1549+
parentObject[key]=mappedValue;
1550+
}
15361551

15371552
// If this is the root object for a model reference, where `handler.value`
15381553
// is a stale `null`, the resolved value can be used directly.
@@ -1799,7 +1814,9 @@ function loadServerReference<A: Iterable<any>, T>(
17991814
response._encodeFormAction,
18001815
);
18011816

1802-
parentObject[key]=resolvedValue;
1817+
if(key!==__PROTO__){
1818+
parentObject[key]=resolvedValue;
1819+
}
18031820

18041821
// If this is the root object for a model reference, where `handler.value`
18051822
// is a stale `null`, the resolved value can be used directly.
@@ -2177,29 +2194,31 @@ function defineLazyGetter<T>(
21772194
): any {
21782195
// We don't immediately initialize it even if it's resolved.
21792196
// Instead, we wait for the getter to get accessed.
2180-
Object.defineProperty(parentObject,key,{
2181-
get: function(){
2182-
if(chunk.status===RESOLVED_MODEL){
2183-
// If it was now resolved, then we initialize it. This may then discover
2184-
// a new set of lazy references that are then asked for eagerly in case
2185-
// we get that deep.
2186-
initializeModelChunk(chunk);
2187-
}
2188-
switch(chunk.status){
2189-
caseINITIALIZED: {
2190-
returnchunk.value;
2197+
if(key!==__PROTO__){
2198+
Object.defineProperty(parentObject,key,{
2199+
get: function(){
2200+
if(chunk.status===RESOLVED_MODEL){
2201+
// If it was now resolved, then we initialize it. This may then discover
2202+
// a new set of lazy references that are then asked for eagerly in case
2203+
// we get that deep.
2204+
initializeModelChunk(chunk);
21912205
}
2192-
caseERRORED:
2193-
throwchunk.reason;
2194-
}
2195-
// Otherwise, we didn't have enough time to load the object before it was
2196-
// accessed or the connection closed. So we just log that it was omitted.
2197-
// TODO: We should ideally throw here to indicate a difference.
2198-
returnOMITTED_PROP_ERROR;
2199-
},
2200-
enumerable: true,
2201-
configurable: false,
2202-
});
2206+
switch(chunk.status){
2207+
caseINITIALIZED: {
2208+
returnchunk.value;
2209+
}
2210+
caseERRORED:
2211+
throwchunk.reason;
2212+
}
2213+
// Otherwise, we didn't have enough time to load the object before it was
2214+
// accessed or the connection closed. So we just log that it was omitted.
2215+
// TODO: We should ideally throw here to indicate a difference.
2216+
returnOMITTED_PROP_ERROR;
2217+
},
2218+
enumerable: true,
2219+
configurable: false,
2220+
});
2221+
}
22032222
return null;
22042223
}
22052224

@@ -2510,14 +2529,16 @@ function parseModelString(
25102529
// In DEV mode we encode omitted objects in logs as a getter that throws
25112530
// so that when you try to access it on the client, you know why that
25122531
// happened.
2513-
Object.defineProperty(parentObject,key,{
2514-
get: function(){
2515-
// TODO: We should ideally throw here to indicate a difference.
2516-
returnOMITTED_PROP_ERROR;
2517-
},
2518-
enumerable: true,
2519-
configurable: false,
2520-
});
2532+
if(key!==__PROTO__){
2533+
Object.defineProperty(parentObject,key,{
2534+
get: function(){
2535+
// TODO: We should ideally throw here to indicate a difference.
2536+
returnOMITTED_PROP_ERROR;
2537+
},
2538+
enumerable: true,
2539+
configurable: false,
2540+
});
2541+
}
25212542
returnnull;
25222543
}
25232544
// Fallthrough
@@ -5143,6 +5164,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
51435164
function createFromJSONCallback(response: Response) {
51445165
// $FlowFixMe[missing-this-annot]
51455166
returnfunction(key: string,value: JSONValue){
5167+
if(key===__PROTO__){
5168+
returnundefined;
5169+
}
51465170
if(typeofvalue==='string'){
51475171
// We can't use .bind here because we need the "this" value.
51485172
returnparseModelString(response,this,key,value);

‎packages/react-client/src/ReactFlightReplyClient.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export type ReactServerValue =
9595

9696
type ReactServerObject = {+[key: string]: ReactServerValue};
9797

98+
const __PROTO__ = '__proto__';
99+
98100
function serializeByValueID(id: number): string {
99101
return'$'+id.toString(16);
100102
}
@@ -361,6 +363,15 @@ export function processReply(
361363
): ReactJSONValue {
362364
constparent=this;
363365

366+
if(__DEV__){
367+
if(key===__PROTO__){
368+
console.error(
369+
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
370+
describeObjectForErrorMessage(parent,key),
371+
);
372+
}
373+
}
374+
364375
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
365376
if(__DEV__){
366377
// $FlowFixMe[incompatible-use]
@@ -780,6 +791,10 @@ export function processReply(
780791
if (typeof value === 'function') {
781792
constreferenceClosure=knownServerReferences.get(value);
782793
if(referenceClosure!==undefined){
794+
const existingReference =writtenObjects.get(value);
795+
if(existingReference!==undefined){
796+
return existingReference;
797+
}
783798
const{id, bound}=referenceClosure;
784799
constreferenceClosureJSON=JSON.stringify({id, bound},resolveToJSON);
785800
if(formData===null){
@@ -789,7 +804,10 @@ export function processReply(
789804
// The reference to this function came from the same client so we can pass it back.
790805
constrefId=nextPartId++;
791806
formData.set(formFieldPrefix+refId,referenceClosureJSON);
792-
returnserializeServerReferenceID(refId);
807+
constserverReferenceId=serializeServerReferenceID(refId);
808+
// Store the server reference ID for deduplication.
809+
writtenObjects.set(value,serverReferenceId);
810+
returnserverReferenceId;
793811
}
794812
if (temporaryReferences !== undefined &&key.indexOf(':')===-1){
795813
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.

‎packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,17 @@ function prerenderToNodeStream(
332332
functiondecodeReplyFromBusboy<T>(
333333
busboyStream: Busboy,
334334
moduleBasePath: ServerManifest,
335-
options?: {temporaryReferences?: TemporaryReferenceSet},
335+
options?: {
336+
temporaryReferences?: TemporaryReferenceSet,
337+
arraySizeLimit?: number,
338+
},
336339
): Thenable<T>{
337340
const response =createResponse(
338341
moduleBasePath,
339342
'',
340343
options ? options.temporaryReferences : undefined,
344+
undefined,
345+
options ? options.arraySizeLimit : undefined,
341346
);
342347
letpendingFiles=0;
343348
constqueuedFields: Array<string>=[];
@@ -403,7 +408,10 @@ function decodeReplyFromBusboy<T>(
403408
functiondecodeReply<T>(
404409
body: string|FormData,
405410
moduleBasePath: ServerManifest,
406-
options?: {temporaryReferences?: TemporaryReferenceSet},
411+
options?: {
412+
temporaryReferences?: TemporaryReferenceSet,
413+
arraySizeLimit?: number,
414+
},
407415
): Thenable<T>{
408416
if(typeofbody=== 'string'){
409417
constform=newFormData();
@@ -415,6 +423,7 @@ function decodeReply<T>(
415423
'',
416424
options ? options.temporaryReferences : undefined,
417425
body,
426+
options ? options.arraySizeLimit : undefined,
418427
);
419428
constroot=getRoot<T>(response);
420429
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ export function registerServerActions(manifest: ServerManifest) {
248248

249249
exportfunctiondecodeReply<T>(
250250
body: string|FormData,
251-
options?: {temporaryReferences?: TemporaryReferenceSet},
251+
options?: {
252+
temporaryReferences?: TemporaryReferenceSet,
253+
arraySizeLimit?: number,
254+
},
252255
): Thenable<T>{
253256
if(typeofbody==='string'){
254257
constform=newFormData();
@@ -260,6 +263,7 @@ export function decodeReply<T>(
260263
'',
261264
options ? options.temporaryReferences : undefined,
262265
body,
266+
options ? options.arraySizeLimit : undefined,
263267
);
264268
constroot=getRoot<T>(response);
265269
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ export function registerServerActions(manifest: ServerManifest) {
253253

254254
exportfunctiondecodeReply<T>(
255255
body: string|FormData,
256-
options?: {temporaryReferences?: TemporaryReferenceSet},
256+
options?: {
257+
temporaryReferences?: TemporaryReferenceSet,
258+
arraySizeLimit?: number,
259+
},
257260
): Thenable<T>{
258261
if(typeofbody==='string'){
259262
constform=newFormData();
@@ -265,6 +268,7 @@ export function decodeReply<T>(
265268
'',
266269
options ? options.temporaryReferences : undefined,
267270
body,
271+
options ? options.arraySizeLimit : undefined,
268272
);
269273
constroot=getRoot<T>(response);
270274
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,17 @@ export function registerServerActions(manifest: ServerManifest) {
562562

563563
exportfunctiondecodeReplyFromBusboy<T>(
564564
busboyStream: Busboy,
565-
options?: {temporaryReferences?: TemporaryReferenceSet},
565+
options?: {
566+
temporaryReferences?: TemporaryReferenceSet,
567+
arraySizeLimit?: number,
568+
},
566569
): Thenable<T>{
567570
const response =createResponse(
568571
serverManifest,
569572
'',
570573
options ? options.temporaryReferences : undefined,
574+
undefined,
575+
options ? options.arraySizeLimit : undefined,
571576
);
572577
letpendingFiles=0;
573578
constqueuedFields: Array<string>=[];
@@ -632,7 +637,10 @@ export function decodeReplyFromBusboy<T>(
632637

633638
exportfunctiondecodeReply<T>(
634639
body: string|FormData,
635-
options?: {temporaryReferences?: TemporaryReferenceSet},
640+
options?: {
641+
temporaryReferences?: TemporaryReferenceSet,
642+
arraySizeLimit?: number,
643+
},
636644
): Thenable<T>{
637645
if(typeofbody=== 'string'){
638646
constform=newFormData();
@@ -644,6 +652,7 @@ export function decodeReply<T>(
644652
'',
645653
options ? options.temporaryReferences : undefined,
646654
body,
655+
options ? options.arraySizeLimit : undefined,
647656
);
648657
constroot=getRoot<T>(response);
649658
close(response);
@@ -652,7 +661,10 @@ export function decodeReply<T>(
652661

653662
exportfunctiondecodeReplyFromAsyncIterable<T>(
654663
iterable: AsyncIterable<[string,string|File]>,
655-
options?: {temporaryReferences?: TemporaryReferenceSet},
664+
options?: {
665+
temporaryReferences?: TemporaryReferenceSet,
666+
arraySizeLimit?: number,
667+
},
656668
): Thenable<T>{
657669
constiterator: AsyncIterator<[string,string|File]>=
658670
iterable[ASYNC_ITERATOR]();
@@ -661,6 +673,8 @@ export function decodeReplyFromAsyncIterable<T>(
661673
serverManifest,
662674
'',
663675
options ? options.temporaryReferences : undefined,
676+
undefined,
677+
options ? options.arraySizeLimit : undefined,
664678
);
665679

666680
functionprogress(

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ function prerender(
242242
functiondecodeReply<T>(
243243
body: string|FormData,
244244
turbopackMap: ServerManifest,
245-
options?: {temporaryReferences?: TemporaryReferenceSet},
245+
options?: {
246+
temporaryReferences?: TemporaryReferenceSet,
247+
arraySizeLimit?: number,
248+
},
246249
): Thenable<T>{
247250
if(typeofbody==='string'){
248251
constform=newFormData();
@@ -254,6 +257,7 @@ function decodeReply<T>(
254257
'',
255258
options ? options.temporaryReferences : undefined,
256259
body,
260+
options ? options.arraySizeLimit : undefined,
257261
);
258262
constroot=getRoot<T>(response);
259263
close(response);

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ function prerender(
247247
functiondecodeReply<T>(
248248
body: string|FormData,
249249
turbopackMap: ServerManifest,
250-
options?: {temporaryReferences?: TemporaryReferenceSet},
250+
options?: {
251+
temporaryReferences?: TemporaryReferenceSet,
252+
arraySizeLimit?: number,
253+
},
251254
): Thenable<T>{
252255
if(typeofbody==='string'){
253256
constform=newFormData();
@@ -259,6 +262,7 @@ function decodeReply<T>(
259262
'',
260263
options ? options.temporaryReferences : undefined,
261264
body,
265+
options ? options.arraySizeLimit : undefined,
262266
);
263267
constroot=getRoot<T>(response);
264268
close(response);
@@ -268,7 +272,10 @@ function decodeReply<T>(
268272
functiondecodeReplyFromAsyncIterable<T>(
269273
iterable: AsyncIterable<[string,string|File]>,
270274
turbopackMap: ServerManifest,
271-
options?: {temporaryReferences?: TemporaryReferenceSet},
275+
options?: {
276+
temporaryReferences?: TemporaryReferenceSet,
277+
arraySizeLimit?: number,
278+
},
272279
): Thenable<T>{
273280
constiterator: AsyncIterator<[string,string|File]>=
274281
iterable[ASYNC_ITERATOR]();
@@ -277,6 +284,8 @@ function decodeReplyFromAsyncIterable<T>(
277284
turbopackMap,
278285
'',
279286
options ? options.temporaryReferences : undefined,
287+
undefined,
288+
options ? options.arraySizeLimit : undefined,
280289
);
281290

282291
functionprogress(

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 63d61c7

Browse files
sebmarkbagegnofflubieowoceunstubbable
committed
Add more DoS mitigations to React Flight Reply, and harden React Flight
Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
1 parent 612e371 commit 63d61c7

17 files changed

Lines changed: 853 additions & 267 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
9898

9999
import{getOwnerStackByComponentInfoInDev}from'shared/ReactComponentInfoStack';
100100

101+
importhasOwnPropertyfrom'shared/hasOwnProperty';
102+
101103
import{injectInternals}from'./ReactFlightClientDevToolsHook';
102104

103105
import{OMITTED_PROP_ERROR}from'shared/ReactFlightPropertyAccess';
@@ -163,6 +165,8 @@ const INITIALIZED = 'fulfilled';
163165
constERRORED='rejected';
164166
constHALTED='halted';// DEV-only. Means it never resolves even if connection closes.
165167

168+
const__PROTO__='__proto__';
169+
166170
typePendingChunk<T>={
167171
status: 'pending',
168172
value: null|Array<InitializationReference|(T=>mixed)>,
@@ -1496,7 +1500,16 @@ function fulfillReference(
14961500
}
14971501
}
14981502
}
1499-
value=value[path[i]];
1503+
constname=path[i];
1504+
if(
1505+
typeofvalue=== 'object' &&
1506+
value!==null&&
1507+
hasOwnProperty.call(value,name)
1508+
){
1509+
value=value[name];
1510+
} else {
1511+
thrownewError('Invalid reference.');
1512+
}
15001513
}
15011514

15021515
while(
@@ -1532,7 +1545,9 @@ function fulfillReference(
15321545
}
15331546

15341547
constmappedValue=map(response,value,parentObject,key);
1535-
parentObject[key]=mappedValue;
1548+
if(key!==__PROTO__){
1549+
parentObject[key]=mappedValue;
1550+
}
15361551

15371552
// If this is the root object for a model reference, where `handler.value`
15381553
// is a stale `null`, the resolved value can be used directly.
@@ -1799,7 +1814,9 @@ function loadServerReference<A: Iterable<any>, T>(
17991814
response._encodeFormAction,
18001815
);
18011816

1802-
parentObject[key]=resolvedValue;
1817+
if(key!==__PROTO__){
1818+
parentObject[key]=resolvedValue;
1819+
}
18031820

18041821
// If this is the root object for a model reference, where `handler.value`
18051822
// is a stale `null`, the resolved value can be used directly.
@@ -2177,29 +2194,31 @@ function defineLazyGetter<T>(
21772194
): any {
21782195
// We don't immediately initialize it even if it's resolved.
21792196
// Instead, we wait for the getter to get accessed.
2180-
Object.defineProperty(parentObject,key,{
2181-
get: function(){
2182-
if(chunk.status===RESOLVED_MODEL){
2183-
// If it was now resolved, then we initialize it. This may then discover
2184-
// a new set of lazy references that are then asked for eagerly in case
2185-
// we get that deep.
2186-
initializeModelChunk(chunk);
2187-
}
2188-
switch(chunk.status){
2189-
caseINITIALIZED: {
2190-
returnchunk.value;
2197+
if(key!==__PROTO__){
2198+
Object.defineProperty(parentObject,key,{
2199+
get: function(){
2200+
if(chunk.status===RESOLVED_MODEL){
2201+
// If it was now resolved, then we initialize it. This may then discover
2202+
// a new set of lazy references that are then asked for eagerly in case
2203+
// we get that deep.
2204+
initializeModelChunk(chunk);
21912205
}
2192-
caseERRORED:
2193-
throwchunk.reason;
2194-
}
2195-
// Otherwise, we didn't have enough time to load the object before it was
2196-
// accessed or the connection closed. So we just log that it was omitted.
2197-
// TODO: We should ideally throw here to indicate a difference.
2198-
returnOMITTED_PROP_ERROR;
2199-
},
2200-
enumerable: true,
2201-
configurable: false,
2202-
});
2206+
switch(chunk.status){
2207+
caseINITIALIZED: {
2208+
returnchunk.value;
2209+
}
2210+
caseERRORED:
2211+
throwchunk.reason;
2212+
}
2213+
// Otherwise, we didn't have enough time to load the object before it was
2214+
// accessed or the connection closed. So we just log that it was omitted.
2215+
// TODO: We should ideally throw here to indicate a difference.
2216+
returnOMITTED_PROP_ERROR;
2217+
},
2218+
enumerable: true,
2219+
configurable: false,
2220+
});
2221+
}
22032222
return null;
22042223
}
22052224

@@ -2510,14 +2529,16 @@ function parseModelString(
25102529
// In DEV mode we encode omitted objects in logs as a getter that throws
25112530
// so that when you try to access it on the client, you know why that
25122531
// happened.
2513-
Object.defineProperty(parentObject,key,{
2514-
get: function(){
2515-
// TODO: We should ideally throw here to indicate a difference.
2516-
returnOMITTED_PROP_ERROR;
2517-
},
2518-
enumerable: true,
2519-
configurable: false,
2520-
});
2532+
if(key!==__PROTO__){
2533+
Object.defineProperty(parentObject,key,{
2534+
get: function(){
2535+
// TODO: We should ideally throw here to indicate a difference.
2536+
returnOMITTED_PROP_ERROR;
2537+
},
2538+
enumerable: true,
2539+
configurable: false,
2540+
});
2541+
}
25212542
returnnull;
25222543
}
25232544
// Fallthrough
@@ -5143,6 +5164,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
51435164
function createFromJSONCallback(response: Response) {
51445165
// $FlowFixMe[missing-this-annot]
51455166
returnfunction(key: string,value: JSONValue){
5167+
if(key===__PROTO__){
5168+
returnundefined;
5169+
}
51465170
if(typeofvalue==='string'){
51475171
// We can't use .bind here because we need the "this" value.
51485172
returnparseModelString(response,this,key,value);

‎packages/react-client/src/ReactFlightReplyClient.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export type ReactServerValue =
9595

9696
type ReactServerObject = {+[key: string]: ReactServerValue};
9797

98+
const __PROTO__ = '__proto__';
99+
98100
function serializeByValueID(id: number): string {
99101
return'$'+id.toString(16);
100102
}
@@ -361,6 +363,15 @@ export function processReply(
361363
): ReactJSONValue {
362364
constparent=this;
363365

366+
if(__DEV__){
367+
if(key===__PROTO__){
368+
console.error(
369+
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
370+
describeObjectForErrorMessage(parent,key),
371+
);
372+
}
373+
}
374+
364375
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
365376
if(__DEV__){
366377
// $FlowFixMe[incompatible-use]
@@ -780,6 +791,10 @@ export function processReply(
780791
if (typeof value === 'function') {
781792
constreferenceClosure=knownServerReferences.get(value);
782793
if(referenceClosure!==undefined){
794+
const existingReference =writtenObjects.get(value);
795+
if(existingReference!==undefined){
796+
return existingReference;
797+
}
783798
const{id, bound}=referenceClosure;
784799
constreferenceClosureJSON=JSON.stringify({id, bound},resolveToJSON);
785800
if(formData===null){
@@ -789,7 +804,10 @@ export function processReply(
789804
// The reference to this function came from the same client so we can pass it back.
790805
constrefId=nextPartId++;
791806
formData.set(formFieldPrefix+refId,referenceClosureJSON);
792-
returnserializeServerReferenceID(refId);
807+
constserverReferenceId=serializeServerReferenceID(refId);
808+
// Store the server reference ID for deduplication.
809+
writtenObjects.set(value,serverReferenceId);
810+
returnserverReferenceId;
793811
}
794812
if (temporaryReferences !== undefined &&key.indexOf(':')===-1){
795813
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.

‎packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,17 @@ function prerenderToNodeStream(
332332
functiondecodeReplyFromBusboy<T>(
333333
busboyStream: Busboy,
334334
moduleBasePath: ServerManifest,
335-
options?: {temporaryReferences?: TemporaryReferenceSet},
335+
options?: {
336+
temporaryReferences?: TemporaryReferenceSet,
337+
arraySizeLimit?: number,
338+
},
336339
): Thenable<T>{
337340
const response =createResponse(
338341
moduleBasePath,
339342
'',
340343
options ? options.temporaryReferences : undefined,
344+
undefined,
345+
options ? options.arraySizeLimit : undefined,
341346
);
342347
letpendingFiles=0;
343348
constqueuedFields: Array<string>=[];
@@ -403,7 +408,10 @@ function decodeReplyFromBusboy<T>(
403408
functiondecodeReply<T>(
404409
body: string|FormData,
405410
moduleBasePath: ServerManifest,
406-
options?: {temporaryReferences?: TemporaryReferenceSet},
411+
options?: {
412+
temporaryReferences?: TemporaryReferenceSet,
413+
arraySizeLimit?: number,
414+
},
407415
): Thenable<T>{
408416
if(typeofbody=== 'string'){
409417
constform=newFormData();
@@ -415,6 +423,7 @@ function decodeReply<T>(
415423
'',
416424
options ? options.temporaryReferences : undefined,
417425
body,
426+
options ? options.arraySizeLimit : undefined,
418427
);
419428
constroot=getRoot<T>(response);
420429
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ export function registerServerActions(manifest: ServerManifest) {
248248

249249
exportfunctiondecodeReply<T>(
250250
body: string|FormData,
251-
options?: {temporaryReferences?: TemporaryReferenceSet},
251+
options?: {
252+
temporaryReferences?: TemporaryReferenceSet,
253+
arraySizeLimit?: number,
254+
},
252255
): Thenable<T>{
253256
if(typeofbody==='string'){
254257
constform=newFormData();
@@ -260,6 +263,7 @@ export function decodeReply<T>(
260263
'',
261264
options ? options.temporaryReferences : undefined,
262265
body,
266+
options ? options.arraySizeLimit : undefined,
263267
);
264268
constroot=getRoot<T>(response);
265269
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ export function registerServerActions(manifest: ServerManifest) {
253253

254254
exportfunctiondecodeReply<T>(
255255
body: string|FormData,
256-
options?: {temporaryReferences?: TemporaryReferenceSet},
256+
options?: {
257+
temporaryReferences?: TemporaryReferenceSet,
258+
arraySizeLimit?: number,
259+
},
257260
): Thenable<T>{
258261
if(typeofbody==='string'){
259262
constform=newFormData();
@@ -265,6 +268,7 @@ export function decodeReply<T>(
265268
'',
266269
options ? options.temporaryReferences : undefined,
267270
body,
271+
options ? options.arraySizeLimit : undefined,
268272
);
269273
constroot=getRoot<T>(response);
270274
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,17 @@ export function registerServerActions(manifest: ServerManifest) {
562562

563563
exportfunctiondecodeReplyFromBusboy<T>(
564564
busboyStream: Busboy,
565-
options?: {temporaryReferences?: TemporaryReferenceSet},
565+
options?: {
566+
temporaryReferences?: TemporaryReferenceSet,
567+
arraySizeLimit?: number,
568+
},
566569
): Thenable<T>{
567570
const response =createResponse(
568571
serverManifest,
569572
'',
570573
options ? options.temporaryReferences : undefined,
574+
undefined,
575+
options ? options.arraySizeLimit : undefined,
571576
);
572577
letpendingFiles=0;
573578
constqueuedFields: Array<string>=[];
@@ -632,7 +637,10 @@ export function decodeReplyFromBusboy<T>(
632637

633638
exportfunctiondecodeReply<T>(
634639
body: string|FormData,
635-
options?: {temporaryReferences?: TemporaryReferenceSet},
640+
options?: {
641+
temporaryReferences?: TemporaryReferenceSet,
642+
arraySizeLimit?: number,
643+
},
636644
): Thenable<T>{
637645
if(typeofbody=== 'string'){
638646
constform=newFormData();
@@ -644,6 +652,7 @@ export function decodeReply<T>(
644652
'',
645653
options ? options.temporaryReferences : undefined,
646654
body,
655+
options ? options.arraySizeLimit : undefined,
647656
);
648657
constroot=getRoot<T>(response);
649658
close(response);
@@ -652,7 +661,10 @@ export function decodeReply<T>(
652661

653662
exportfunctiondecodeReplyFromAsyncIterable<T>(
654663
iterable: AsyncIterable<[string,string|File]>,
655-
options?: {temporaryReferences?: TemporaryReferenceSet},
664+
options?: {
665+
temporaryReferences?: TemporaryReferenceSet,
666+
arraySizeLimit?: number,
667+
},
656668
): Thenable<T>{
657669
constiterator: AsyncIterator<[string,string|File]>=
658670
iterable[ASYNC_ITERATOR]();
@@ -661,6 +673,8 @@ export function decodeReplyFromAsyncIterable<T>(
661673
serverManifest,
662674
'',
663675
options ? options.temporaryReferences : undefined,
676+
undefined,
677+
options ? options.arraySizeLimit : undefined,
664678
);
665679

666680
functionprogress(

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ function prerender(
242242
functiondecodeReply<T>(
243243
body: string|FormData,
244244
turbopackMap: ServerManifest,
245-
options?: {temporaryReferences?: TemporaryReferenceSet},
245+
options?: {
246+
temporaryReferences?: TemporaryReferenceSet,
247+
arraySizeLimit?: number,
248+
},
246249
): Thenable<T>{
247250
if(typeofbody==='string'){
248251
constform=newFormData();
@@ -254,6 +257,7 @@ function decodeReply<T>(
254257
'',
255258
options ? options.temporaryReferences : undefined,
256259
body,
260+
options ? options.arraySizeLimit : undefined,
257261
);
258262
constroot=getRoot<T>(response);
259263
close(response);

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ function prerender(
247247
functiondecodeReply<T>(
248248
body: string|FormData,
249249
turbopackMap: ServerManifest,
250-
options?: {temporaryReferences?: TemporaryReferenceSet},
250+
options?: {
251+
temporaryReferences?: TemporaryReferenceSet,
252+
arraySizeLimit?: number,
253+
},
251254
): Thenable<T>{
252255
if(typeofbody==='string'){
253256
constform=newFormData();
@@ -259,6 +262,7 @@ function decodeReply<T>(
259262
'',
260263
options ? options.temporaryReferences : undefined,
261264
body,
265+
options ? options.arraySizeLimit : undefined,
262266
);
263267
constroot=getRoot<T>(response);
264268
close(response);
@@ -268,7 +272,10 @@ function decodeReply<T>(
268272
functiondecodeReplyFromAsyncIterable<T>(
269273
iterable: AsyncIterable<[string,string|File]>,
270274
turbopackMap: ServerManifest,
271-
options?: {temporaryReferences?: TemporaryReferenceSet},
275+
options?: {
276+
temporaryReferences?: TemporaryReferenceSet,
277+
arraySizeLimit?: number,
278+
},
272279
): Thenable<T>{
273280
constiterator: AsyncIterator<[string,string|File]>=
274281
iterable[ASYNC_ITERATOR]();
@@ -277,6 +284,8 @@ function decodeReplyFromAsyncIterable<T>(
277284
turbopackMap,
278285
'',
279286
options ? options.temporaryReferences : undefined,
287+
undefined,
288+
options ? options.arraySizeLimit : undefined,
280289
);
281290

282291
functionprogress(

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit 63d61c7

Browse files
sebmarkbagegnofflubieowoceunstubbable
committed
Add more DoS mitigations to React Flight Reply, and harden React Flight
Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
1 parent 612e371 commit 63d61c7

17 files changed

Lines changed: 853 additions & 267 deletions

File tree

‎packages/react-client/src/ReactFlightClient.js‎

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
9898

9999
import{getOwnerStackByComponentInfoInDev}from'shared/ReactComponentInfoStack';
100100

101+
importhasOwnPropertyfrom'shared/hasOwnProperty';
102+
101103
import{injectInternals}from'./ReactFlightClientDevToolsHook';
102104

103105
import{OMITTED_PROP_ERROR}from'shared/ReactFlightPropertyAccess';
@@ -163,6 +165,8 @@ const INITIALIZED = 'fulfilled';
163165
constERRORED='rejected';
164166
constHALTED='halted';// DEV-only. Means it never resolves even if connection closes.
165167

168+
const__PROTO__='__proto__';
169+
166170
typePendingChunk<T>={
167171
status: 'pending',
168172
value: null|Array<InitializationReference|(T=>mixed)>,
@@ -1496,7 +1500,16 @@ function fulfillReference(
14961500
}
14971501
}
14981502
}
1499-
value=value[path[i]];
1503+
constname=path[i];
1504+
if(
1505+
typeofvalue=== 'object' &&
1506+
value!==null&&
1507+
hasOwnProperty.call(value,name)
1508+
){
1509+
value=value[name];
1510+
} else {
1511+
thrownewError('Invalid reference.');
1512+
}
15001513
}
15011514

15021515
while(
@@ -1532,7 +1545,9 @@ function fulfillReference(
15321545
}
15331546

15341547
constmappedValue=map(response,value,parentObject,key);
1535-
parentObject[key]=mappedValue;
1548+
if(key!==__PROTO__){
1549+
parentObject[key]=mappedValue;
1550+
}
15361551

15371552
// If this is the root object for a model reference, where `handler.value`
15381553
// is a stale `null`, the resolved value can be used directly.
@@ -1799,7 +1814,9 @@ function loadServerReference<A: Iterable<any>, T>(
17991814
response._encodeFormAction,
18001815
);
18011816

1802-
parentObject[key]=resolvedValue;
1817+
if(key!==__PROTO__){
1818+
parentObject[key]=resolvedValue;
1819+
}
18031820

18041821
// If this is the root object for a model reference, where `handler.value`
18051822
// is a stale `null`, the resolved value can be used directly.
@@ -2177,29 +2194,31 @@ function defineLazyGetter<T>(
21772194
): any {
21782195
// We don't immediately initialize it even if it's resolved.
21792196
// Instead, we wait for the getter to get accessed.
2180-
Object.defineProperty(parentObject,key,{
2181-
get: function(){
2182-
if(chunk.status===RESOLVED_MODEL){
2183-
// If it was now resolved, then we initialize it. This may then discover
2184-
// a new set of lazy references that are then asked for eagerly in case
2185-
// we get that deep.
2186-
initializeModelChunk(chunk);
2187-
}
2188-
switch(chunk.status){
2189-
caseINITIALIZED: {
2190-
returnchunk.value;
2197+
if(key!==__PROTO__){
2198+
Object.defineProperty(parentObject,key,{
2199+
get: function(){
2200+
if(chunk.status===RESOLVED_MODEL){
2201+
// If it was now resolved, then we initialize it. This may then discover
2202+
// a new set of lazy references that are then asked for eagerly in case
2203+
// we get that deep.
2204+
initializeModelChunk(chunk);
21912205
}
2192-
caseERRORED:
2193-
throwchunk.reason;
2194-
}
2195-
// Otherwise, we didn't have enough time to load the object before it was
2196-
// accessed or the connection closed. So we just log that it was omitted.
2197-
// TODO: We should ideally throw here to indicate a difference.
2198-
returnOMITTED_PROP_ERROR;
2199-
},
2200-
enumerable: true,
2201-
configurable: false,
2202-
});
2206+
switch(chunk.status){
2207+
caseINITIALIZED: {
2208+
returnchunk.value;
2209+
}
2210+
caseERRORED:
2211+
throwchunk.reason;
2212+
}
2213+
// Otherwise, we didn't have enough time to load the object before it was
2214+
// accessed or the connection closed. So we just log that it was omitted.
2215+
// TODO: We should ideally throw here to indicate a difference.
2216+
returnOMITTED_PROP_ERROR;
2217+
},
2218+
enumerable: true,
2219+
configurable: false,
2220+
});
2221+
}
22032222
return null;
22042223
}
22052224

@@ -2510,14 +2529,16 @@ function parseModelString(
25102529
// In DEV mode we encode omitted objects in logs as a getter that throws
25112530
// so that when you try to access it on the client, you know why that
25122531
// happened.
2513-
Object.defineProperty(parentObject,key,{
2514-
get: function(){
2515-
// TODO: We should ideally throw here to indicate a difference.
2516-
returnOMITTED_PROP_ERROR;
2517-
},
2518-
enumerable: true,
2519-
configurable: false,
2520-
});
2532+
if(key!==__PROTO__){
2533+
Object.defineProperty(parentObject,key,{
2534+
get: function(){
2535+
// TODO: We should ideally throw here to indicate a difference.
2536+
returnOMITTED_PROP_ERROR;
2537+
},
2538+
enumerable: true,
2539+
configurable: false,
2540+
});
2541+
}
25212542
returnnull;
25222543
}
25232544
// Fallthrough
@@ -5143,6 +5164,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
51435164
function createFromJSONCallback(response: Response) {
51445165
// $FlowFixMe[missing-this-annot]
51455166
returnfunction(key: string,value: JSONValue){
5167+
if(key===__PROTO__){
5168+
returnundefined;
5169+
}
51465170
if(typeofvalue==='string'){
51475171
// We can't use .bind here because we need the "this" value.
51485172
returnparseModelString(response,this,key,value);

‎packages/react-client/src/ReactFlightReplyClient.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export type ReactServerValue =
9595

9696
type ReactServerObject = {+[key: string]: ReactServerValue};
9797

98+
const __PROTO__ = '__proto__';
99+
98100
function serializeByValueID(id: number): string {
99101
return'$'+id.toString(16);
100102
}
@@ -361,6 +363,15 @@ export function processReply(
361363
): ReactJSONValue {
362364
constparent=this;
363365

366+
if(__DEV__){
367+
if(key===__PROTO__){
368+
console.error(
369+
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
370+
describeObjectForErrorMessage(parent,key),
371+
);
372+
}
373+
}
374+
364375
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
365376
if(__DEV__){
366377
// $FlowFixMe[incompatible-use]
@@ -780,6 +791,10 @@ export function processReply(
780791
if (typeof value === 'function') {
781792
constreferenceClosure=knownServerReferences.get(value);
782793
if(referenceClosure!==undefined){
794+
const existingReference =writtenObjects.get(value);
795+
if(existingReference!==undefined){
796+
return existingReference;
797+
}
783798
const{id, bound}=referenceClosure;
784799
constreferenceClosureJSON=JSON.stringify({id, bound},resolveToJSON);
785800
if(formData===null){
@@ -789,7 +804,10 @@ export function processReply(
789804
// The reference to this function came from the same client so we can pass it back.
790805
constrefId=nextPartId++;
791806
formData.set(formFieldPrefix+refId,referenceClosureJSON);
792-
returnserializeServerReferenceID(refId);
807+
constserverReferenceId=serializeServerReferenceID(refId);
808+
// Store the server reference ID for deduplication.
809+
writtenObjects.set(value,serverReferenceId);
810+
returnserverReferenceId;
793811
}
794812
if (temporaryReferences !== undefined &&key.indexOf(':')===-1){
795813
// TODO: If the property name contains a colon, we don't dedupe. Escape instead.

‎packages/react-server-dom-esm/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,17 @@ function prerenderToNodeStream(
332332
functiondecodeReplyFromBusboy<T>(
333333
busboyStream: Busboy,
334334
moduleBasePath: ServerManifest,
335-
options?: {temporaryReferences?: TemporaryReferenceSet},
335+
options?: {
336+
temporaryReferences?: TemporaryReferenceSet,
337+
arraySizeLimit?: number,
338+
},
336339
): Thenable<T>{
337340
const response =createResponse(
338341
moduleBasePath,
339342
'',
340343
options ? options.temporaryReferences : undefined,
344+
undefined,
345+
options ? options.arraySizeLimit : undefined,
341346
);
342347
letpendingFiles=0;
343348
constqueuedFields: Array<string>=[];
@@ -403,7 +408,10 @@ function decodeReplyFromBusboy<T>(
403408
functiondecodeReply<T>(
404409
body: string|FormData,
405410
moduleBasePath: ServerManifest,
406-
options?: {temporaryReferences?: TemporaryReferenceSet},
411+
options?: {
412+
temporaryReferences?: TemporaryReferenceSet,
413+
arraySizeLimit?: number,
414+
},
407415
): Thenable<T>{
408416
if(typeofbody=== 'string'){
409417
constform=newFormData();
@@ -415,6 +423,7 @@ function decodeReply<T>(
415423
'',
416424
options ? options.temporaryReferences : undefined,
417425
body,
426+
options ? options.arraySizeLimit : undefined,
418427
);
419428
constroot=getRoot<T>(response);
420429
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@ export function registerServerActions(manifest: ServerManifest) {
248248

249249
exportfunctiondecodeReply<T>(
250250
body: string|FormData,
251-
options?: {temporaryReferences?: TemporaryReferenceSet},
251+
options?: {
252+
temporaryReferences?: TemporaryReferenceSet,
253+
arraySizeLimit?: number,
254+
},
252255
): Thenable<T>{
253256
if(typeofbody==='string'){
254257
constform=newFormData();
@@ -260,6 +263,7 @@ export function decodeReply<T>(
260263
'',
261264
options ? options.temporaryReferences : undefined,
262265
body,
266+
options ? options.arraySizeLimit : undefined,
263267
);
264268
constroot=getRoot<T>(response);
265269
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ export function registerServerActions(manifest: ServerManifest) {
253253

254254
exportfunctiondecodeReply<T>(
255255
body: string|FormData,
256-
options?: {temporaryReferences?: TemporaryReferenceSet},
256+
options?: {
257+
temporaryReferences?: TemporaryReferenceSet,
258+
arraySizeLimit?: number,
259+
},
257260
): Thenable<T>{
258261
if(typeofbody==='string'){
259262
constform=newFormData();
@@ -265,6 +268,7 @@ export function decodeReply<T>(
265268
'',
266269
options ? options.temporaryReferences : undefined,
267270
body,
271+
options ? options.arraySizeLimit : undefined,
268272
);
269273
constroot=getRoot<T>(response);
270274
close(response);

‎packages/react-server-dom-parcel/src/server/ReactFlightDOMServerNode.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,17 @@ export function registerServerActions(manifest: ServerManifest) {
562562

563563
exportfunctiondecodeReplyFromBusboy<T>(
564564
busboyStream: Busboy,
565-
options?: {temporaryReferences?: TemporaryReferenceSet},
565+
options?: {
566+
temporaryReferences?: TemporaryReferenceSet,
567+
arraySizeLimit?: number,
568+
},
566569
): Thenable<T>{
567570
const response =createResponse(
568571
serverManifest,
569572
'',
570573
options ? options.temporaryReferences : undefined,
574+
undefined,
575+
options ? options.arraySizeLimit : undefined,
571576
);
572577
letpendingFiles=0;
573578
constqueuedFields: Array<string>=[];
@@ -632,7 +637,10 @@ export function decodeReplyFromBusboy<T>(
632637

633638
exportfunctiondecodeReply<T>(
634639
body: string|FormData,
635-
options?: {temporaryReferences?: TemporaryReferenceSet},
640+
options?: {
641+
temporaryReferences?: TemporaryReferenceSet,
642+
arraySizeLimit?: number,
643+
},
636644
): Thenable<T>{
637645
if(typeofbody=== 'string'){
638646
constform=newFormData();
@@ -644,6 +652,7 @@ export function decodeReply<T>(
644652
'',
645653
options ? options.temporaryReferences : undefined,
646654
body,
655+
options ? options.arraySizeLimit : undefined,
647656
);
648657
constroot=getRoot<T>(response);
649658
close(response);
@@ -652,7 +661,10 @@ export function decodeReply<T>(
652661

653662
exportfunctiondecodeReplyFromAsyncIterable<T>(
654663
iterable: AsyncIterable<[string,string|File]>,
655-
options?: {temporaryReferences?: TemporaryReferenceSet},
664+
options?: {
665+
temporaryReferences?: TemporaryReferenceSet,
666+
arraySizeLimit?: number,
667+
},
656668
): Thenable<T>{
657669
constiterator: AsyncIterator<[string,string|File]>=
658670
iterable[ASYNC_ITERATOR]();
@@ -661,6 +673,8 @@ export function decodeReplyFromAsyncIterable<T>(
661673
serverManifest,
662674
'',
663675
options ? options.temporaryReferences : undefined,
676+
undefined,
677+
options ? options.arraySizeLimit : undefined,
664678
);
665679

666680
functionprogress(

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerBrowser.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ function prerender(
242242
functiondecodeReply<T>(
243243
body: string|FormData,
244244
turbopackMap: ServerManifest,
245-
options?: {temporaryReferences?: TemporaryReferenceSet},
245+
options?: {
246+
temporaryReferences?: TemporaryReferenceSet,
247+
arraySizeLimit?: number,
248+
},
246249
): Thenable<T>{
247250
if(typeofbody==='string'){
248251
constform=newFormData();
@@ -254,6 +257,7 @@ function decodeReply<T>(
254257
'',
255258
options ? options.temporaryReferences : undefined,
256259
body,
260+
options ? options.arraySizeLimit : undefined,
257261
);
258262
constroot=getRoot<T>(response);
259263
close(response);

‎packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerEdge.js‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ function prerender(
247247
functiondecodeReply<T>(
248248
body: string|FormData,
249249
turbopackMap: ServerManifest,
250-
options?: {temporaryReferences?: TemporaryReferenceSet},
250+
options?: {
251+
temporaryReferences?: TemporaryReferenceSet,
252+
arraySizeLimit?: number,
253+
},
251254
): Thenable<T>{
252255
if(typeofbody==='string'){
253256
constform=newFormData();
@@ -259,6 +262,7 @@ function decodeReply<T>(
259262
'',
260263
options ? options.temporaryReferences : undefined,
261264
body,
265+
options ? options.arraySizeLimit : undefined,
262266
);
263267
constroot=getRoot<T>(response);
264268
close(response);
@@ -268,7 +272,10 @@ function decodeReply<T>(
268272
functiondecodeReplyFromAsyncIterable<T>(
269273
iterable: AsyncIterable<[string,string|File]>,
270274
turbopackMap: ServerManifest,
271-
options?: {temporaryReferences?: TemporaryReferenceSet},
275+
options?: {
276+
temporaryReferences?: TemporaryReferenceSet,
277+
arraySizeLimit?: number,
278+
},
272279
): Thenable<T>{
273280
constiterator: AsyncIterator<[string,string|File]>=
274281
iterable[ASYNC_ITERATOR]();
@@ -277,6 +284,8 @@ function decodeReplyFromAsyncIterable<T>(
277284
turbopackMap,
278285
'',
279286
options ? options.temporaryReferences : undefined,
287+
undefined,
288+
options ? options.arraySizeLimit : undefined,
280289
);
281290

282291
functionprogress(

0 commit comments

Comments
 (0)