Skip to content

Commit de58cca

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 d8946cb commit de58cca

13 files changed

Lines changed: 771 additions & 232 deletions

File tree

packages/react-client/src/ReactFlightClient.js

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
7979

8080
import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack';
8181

82+
import hasOwnProperty from 'shared/hasOwnProperty';
83+
8284
import {injectInternals} from './ReactFlightClientDevToolsHook';
8385

8486
import ReactVersion from 'shared/ReactVersion';
@@ -135,6 +137,8 @@ const RESOLVED_MODULE = 'resolved_module';
135137
const INITIALIZED = 'fulfilled';
136138
const ERRORED = 'rejected';
137139

140+
const __PROTO__ = '__proto__';
141+
138142
type PendingChunk<T> = {
139143
status: 'pending',
140144
value: null | Array<(T) => mixed>,
@@ -924,10 +928,21 @@ function waitForReference<T>(
924928
return;
925929
}
926930
}
927-
value = value[path[i]];
931+
const name = path[i];
932+
if (
933+
typeof value === 'object' &&
934+
value !== null &&
935+
hasOwnProperty.call(value, name)
936+
) {
937+
value = value[name];
938+
} else {
939+
throw new Error('Invalid reference.');
940+
}
928941
}
929942
const mappedValue = map(response, value, parentObject, key);
930-
parentObject[key] = mappedValue;
943+
if (key !== __PROTO__) {
944+
parentObject[key] = mappedValue;
945+
}
931946

932947
// If this is the root object for a model reference, where `handler.value`
933948
// is a stale `null`, the resolved value can be used directly.
@@ -1093,7 +1108,9 @@ function loadServerReference<A: Iterable<any>, T>(
10931108
resolvedValue = resolvedValue.bind.apply(resolvedValue, boundArgs);
10941109
}
10951110

1096-
parentObject[key] = resolvedValue;
1111+
if (key !== __PROTO__) {
1112+
parentObject[key] = resolvedValue;
1113+
}
10971114

10981115
// If this is the root object for a model reference, where `handler.value`
10991116
// is a stale `null`, the resolved value can be used directly.
@@ -1499,18 +1516,20 @@ function parseModelString(
14991516
// In DEV mode we encode omitted objects in logs as a getter that throws
15001517
// so that when you try to access it on the client, you know why that
15011518
// happened.
1502-
Object.defineProperty(parentObject, key, {
1503-
get: function () {
1504-
// TODO: We should ideally throw here to indicate a difference.
1505-
return (
1506-
'This object has been omitted by React in the console log ' +
1507-
'to avoid sending too much data from the server. Try logging smaller ' +
1508-
'or more specific objects.'
1509-
);
1510-
},
1511-
enumerable: true,
1512-
configurable: false,
1513-
});
1519+
if (key !== __PROTO__) {
1520+
Object.defineProperty(parentObject, key, {
1521+
get: function () {
1522+
// TODO: We should ideally throw here to indicate a difference.
1523+
return (
1524+
'This object has been omitted by React in the console log ' +
1525+
'to avoid sending too much data from the server. Try logging smaller ' +
1526+
'or more specific objects.'
1527+
);
1528+
},
1529+
enumerable: true,
1530+
configurable: false,
1531+
});
1532+
}
15141533
return null;
15151534
}
15161535
// Fallthrough
@@ -3144,6 +3163,9 @@ function parseModel<T>(response: Response, json: UninitializedModel): T {
31443163
function createFromJSONCallback(response: Response) {
31453164
// $FlowFixMe[missing-this-annot]
31463165
return function (key: string, value: JSONValue) {
3166+
if (key === __PROTO__) {
3167+
return undefined;
3168+
}
31473169
if (typeof value === 'string') {
31483170
// We can't use .bind here because we need the "this" value.
31493171
return parseModelString(response, this, key, value);

packages/react-client/src/ReactFlightReplyClient.js

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

9999
type ReactServerObject = {+[key: string]: ReactServerValue};
100100

101+
const __PROTO__ = '__proto__';
102+
101103
function serializeByValueID(id: number): string {
102104
return '$' + id.toString(16);
103105
}
@@ -364,6 +366,15 @@ export function processReply(
364366
): ReactJSONValue {
365367
const parent = this;
366368

369+
if (__DEV__) {
370+
if (key === __PROTO__) {
371+
console.error(
372+
'Expected not to serialize an object with own property `__proto__`. When parsed this property will be omitted.%s',
373+
describeObjectForErrorMessage(parent, key),
374+
);
375+
}
376+
}
377+
367378
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
368379
if (__DEV__) {
369380
// $FlowFixMe[incompatible-use]
@@ -790,6 +801,10 @@ export function processReply(
790801
if (typeof value === 'function') {
791802
const metaData = knownServerReferences.get(value);
792803
if (metaData !== undefined) {
804+
const existingReference = writtenObjects.get(value);
805+
if (existingReference !== undefined) {
806+
return existingReference;
807+
}
793808
const metaDataJSON = JSON.stringify(metaData, resolveToJSON);
794809
if (formData === null) {
795810
// Upgrade to use FormData to allow us to stream this value.
@@ -798,7 +813,10 @@ export function processReply(
798813
// The reference to this function came from the same client so we can pass it back.
799814
const refId = nextPartId++;
800815
formData.set(formFieldPrefix + refId, metaDataJSON);
801-
return serializeServerReferenceID(refId);
816+
const serverReferenceId = serializeServerReferenceID(refId);
817+
// Store the server reference ID for deduplication.
818+
writtenObjects.set(value, serverReferenceId);
819+
return serverReferenceId;
802820
}
803821
if (temporaryReferences !== undefined && key.indexOf(':') === -1) {
804822
// 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
@@ -206,12 +206,17 @@ function prerenderToNodeStream(
206206
function decodeReplyFromBusboy<T>(
207207
busboyStream: Busboy,
208208
moduleBasePath: ServerManifest,
209-
options?: {temporaryReferences?: TemporaryReferenceSet},
209+
options?: {
210+
temporaryReferences?: TemporaryReferenceSet,
211+
arraySizeLimit?: number,
212+
},
210213
): Thenable<T> {
211214
const response = createResponse(
212215
moduleBasePath,
213216
'',
214217
options ? options.temporaryReferences : undefined,
218+
undefined,
219+
options ? options.arraySizeLimit : undefined,
215220
);
216221
let pendingFiles = 0;
217222
const queuedFields: Array<string> = [];
@@ -277,7 +282,10 @@ function decodeReplyFromBusboy<T>(
277282
function decodeReply<T>(
278283
body: string | FormData,
279284
moduleBasePath: ServerManifest,
280-
options?: {temporaryReferences?: TemporaryReferenceSet},
285+
options?: {
286+
temporaryReferences?: TemporaryReferenceSet,
287+
arraySizeLimit?: number,
288+
},
281289
): Thenable<T> {
282290
if (typeof body === 'string') {
283291
const form = new FormData();
@@ -289,6 +297,7 @@ function decodeReply<T>(
289297
'',
290298
options ? options.temporaryReferences : undefined,
291299
body,
300+
options ? options.arraySizeLimit : undefined,
292301
);
293302
const root = getRoot<T>(response);
294303
close(response);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ function prerender(
165165
function decodeReply<T>(
166166
body: string | FormData,
167167
turbopackMap: ServerManifest,
168-
options?: {temporaryReferences?: TemporaryReferenceSet},
168+
options?: {
169+
temporaryReferences?: TemporaryReferenceSet,
170+
arraySizeLimit?: number,
171+
},
169172
): Thenable<T> {
170173
if (typeof body === 'string') {
171174
const form = new FormData();
@@ -177,6 +180,7 @@ function decodeReply<T>(
177180
'',
178181
options ? options.temporaryReferences : undefined,
179182
body,
183+
options ? options.arraySizeLimit : undefined,
180184
);
181185
const root = getRoot<T>(response);
182186
close(response);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ function prerender(
165165
function decodeReply<T>(
166166
body: string | FormData,
167167
turbopackMap: ServerManifest,
168-
options?: {temporaryReferences?: TemporaryReferenceSet},
168+
options?: {
169+
temporaryReferences?: TemporaryReferenceSet,
170+
arraySizeLimit?: number,
171+
},
169172
): Thenable<T> {
170173
if (typeof body === 'string') {
171174
const form = new FormData();
@@ -177,6 +180,7 @@ function decodeReply<T>(
177180
'',
178181
options ? options.temporaryReferences : undefined,
179182
body,
183+
options ? options.arraySizeLimit : undefined,
180184
);
181185
const root = getRoot<T>(response);
182186
close(response);

packages/react-server-dom-turbopack/src/server/ReactFlightDOMServerNode.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,17 @@ function prerenderToNodeStream(
208208
function decodeReplyFromBusboy<T>(
209209
busboyStream: Busboy,
210210
turbopackMap: ServerManifest,
211-
options?: {temporaryReferences?: TemporaryReferenceSet},
211+
options?: {
212+
temporaryReferences?: TemporaryReferenceSet,
213+
arraySizeLimit?: number,
214+
},
212215
): Thenable<T> {
213216
const response = createResponse(
214217
turbopackMap,
215218
'',
216219
options ? options.temporaryReferences : undefined,
220+
undefined,
221+
options ? options.arraySizeLimit : undefined,
217222
);
218223
let pendingFiles = 0;
219224
const queuedFields: Array<string> = [];
@@ -279,7 +284,10 @@ function decodeReplyFromBusboy<T>(
279284
function decodeReply<T>(
280285
body: string | FormData,
281286
turbopackMap: ServerManifest,
282-
options?: {temporaryReferences?: TemporaryReferenceSet},
287+
options?: {
288+
temporaryReferences?: TemporaryReferenceSet,
289+
arraySizeLimit?: number,
290+
},
283291
): Thenable<T> {
284292
if (typeof body === 'string') {
285293
const form = new FormData();
@@ -291,6 +299,7 @@ function decodeReply<T>(
291299
'',
292300
options ? options.temporaryReferences : undefined,
293301
body,
302+
options ? options.arraySizeLimit : undefined,
294303
);
295304
const root = getRoot<T>(response);
296305
close(response);

packages/react-server-dom-webpack/src/server/ReactFlightDOMServerBrowser.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ function prerender(
165165
function decodeReply<T>(
166166
body: string | FormData,
167167
webpackMap: ServerManifest,
168-
options?: {temporaryReferences?: TemporaryReferenceSet},
168+
options?: {
169+
temporaryReferences?: TemporaryReferenceSet,
170+
arraySizeLimit?: number,
171+
},
169172
): Thenable<T> {
170173
if (typeof body === 'string') {
171174
const form = new FormData();
@@ -177,6 +180,7 @@ function decodeReply<T>(
177180
'',
178181
options ? options.temporaryReferences : undefined,
179182
body,
183+
options ? options.arraySizeLimit : undefined,
180184
);
181185
const root = getRoot<T>(response);
182186
close(response);

packages/react-server-dom-webpack/src/server/ReactFlightDOMServerEdge.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ function prerender(
165165
function decodeReply<T>(
166166
body: string | FormData,
167167
webpackMap: ServerManifest,
168-
options?: {temporaryReferences?: TemporaryReferenceSet},
168+
options?: {
169+
temporaryReferences?: TemporaryReferenceSet,
170+
arraySizeLimit?: number,
171+
},
169172
): Thenable<T> {
170173
if (typeof body === 'string') {
171174
const form = new FormData();
@@ -177,6 +180,7 @@ function decodeReply<T>(
177180
'',
178181
options ? options.temporaryReferences : undefined,
179182
body,
183+
options ? options.arraySizeLimit : undefined,
180184
);
181185
const root = getRoot<T>(response);
182186
close(response);

packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,17 @@ function prerenderToNodeStream(
208208
function decodeReplyFromBusboy<T>(
209209
busboyStream: Busboy,
210210
webpackMap: ServerManifest,
211-
options?: {temporaryReferences?: TemporaryReferenceSet},
211+
options?: {
212+
temporaryReferences?: TemporaryReferenceSet,
213+
arraySizeLimit?: number,
214+
},
212215
): Thenable<T> {
213216
const response = createResponse(
214217
webpackMap,
215218
'',
216219
options ? options.temporaryReferences : undefined,
220+
undefined,
221+
options ? options.arraySizeLimit : undefined,
217222
);
218223
let pendingFiles = 0;
219224
const queuedFields: Array<string> = [];
@@ -279,7 +284,10 @@ function decodeReplyFromBusboy<T>(
279284
function decodeReply<T>(
280285
body: string | FormData,
281286
webpackMap: ServerManifest,
282-
options?: {temporaryReferences?: TemporaryReferenceSet},
287+
options?: {
288+
temporaryReferences?: TemporaryReferenceSet,
289+
arraySizeLimit?: number,
290+
},
283291
): Thenable<T> {
284292
if (typeof body === 'string') {
285293
const form = new FormData();
@@ -291,6 +299,7 @@ function decodeReply<T>(
291299
'',
292300
options ? options.temporaryReferences : undefined,
293301
body,
302+
options ? options.arraySizeLimit : undefined,
294303
);
295304
const root = getRoot<T>(response);
296305
close(response);

0 commit comments

Comments
 (0)