Skip to content

Commit e61a60f

Browse files
authored
[Flight] Enforce "simple object" rule in production (#27502)
We only allow plain objects that can be faithfully serialized and deserialized through JSON to pass through the serialization boundary. It's a bit too expensive to do all the possible checks in production so we do most checks in DEV, so it's still possible to pass an object in production by mistake. This is currently exaggerated by frameworks because the logs on the server aren't visible enough. Even so, it's possible to do a mistake without testing it in DEV or just testing a conditional branch. That might have security implications if that object wasn't supposed to be passed. We can't rely on only checking if the prototype is `Object.prototype` because that wouldn't work with cross-realm objects which is unfortunate. However, if it isn't, we can check wether it has exactly one prototype on the chain which would catch the common error of passing a class instance.
1 parent 1fc5828 commit e61a60f

File tree

7 files changed

+127
-83
lines changed

7 files changed

+127
-83
lines changed

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import {
2929
} from 'shared/ReactSerializationErrors';
3030

3131
import isArray from 'shared/isArray';
32+
import getPrototypeOf from 'shared/getPrototypeOf';
33+
34+
const ObjectPrototype = Object.prototype;
3235

3336
import {usedWithSSR} from './ReactFlightClientConfig';
3437

@@ -227,6 +230,10 @@ export function processReply(
227230
);
228231
return serializePromiseID(promiseId);
229232
}
233+
if (isArray(value)) {
234+
// $FlowFixMe[incompatible-return]
235+
return value;
236+
}
230237
// TODO: Should we the Object.prototype.toString.call() to test for cross-realm objects?
231238
if (value instanceof FormData) {
232239
if (formData === null) {
@@ -263,54 +270,60 @@ export function processReply(
263270
formData.append(formFieldPrefix + setId, partJSON);
264271
return serializeSetID(setId);
265272
}
266-
if (!isArray(value)) {
267-
const iteratorFn = getIteratorFn(value);
268-
if (iteratorFn) {
269-
return Array.from((value: any));
270-
}
273+
const iteratorFn = getIteratorFn(value);
274+
if (iteratorFn) {
275+
return Array.from((value: any));
271276
}
272277

278+
// Verify that this is a simple plain object.
279+
const proto = getPrototypeOf(value);
280+
if (
281+
proto !== ObjectPrototype &&
282+
(proto === null || getPrototypeOf(proto) !== null)
283+
) {
284+
throw new Error(
285+
'Only plain objects, and a few built-ins, can be passed to Server Actions. ' +
286+
'Classes or null prototypes are not supported.',
287+
);
288+
}
273289
if (__DEV__) {
274-
if (value !== null && !isArray(value)) {
275-
// Verify that this is a simple plain object.
276-
if ((value: any).$$typeof === REACT_ELEMENT_TYPE) {
277-
console.error(
278-
'React Element cannot be passed to Server Functions from the Client.%s',
279-
describeObjectForErrorMessage(parent, key),
280-
);
281-
} else if ((value: any).$$typeof === REACT_LAZY_TYPE) {
282-
console.error(
283-
'React Lazy cannot be passed to Server Functions from the Client.%s',
284-
describeObjectForErrorMessage(parent, key),
285-
);
286-
} else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) {
287-
console.error(
288-
'React Context Providers cannot be passed to Server Functions from the Client.%s',
289-
describeObjectForErrorMessage(parent, key),
290-
);
291-
} else if (objectName(value) !== 'Object') {
292-
console.error(
293-
'Only plain objects can be passed to Client Components from Server Components. ' +
294-
'%s objects are not supported.%s',
295-
objectName(value),
296-
describeObjectForErrorMessage(parent, key),
297-
);
298-
} else if (!isSimpleObject(value)) {
290+
if ((value: any).$$typeof === REACT_ELEMENT_TYPE) {
291+
console.error(
292+
'React Element cannot be passed to Server Functions from the Client.%s',
293+
describeObjectForErrorMessage(parent, key),
294+
);
295+
} else if ((value: any).$$typeof === REACT_LAZY_TYPE) {
296+
console.error(
297+
'React Lazy cannot be passed to Server Functions from the Client.%s',
298+
describeObjectForErrorMessage(parent, key),
299+
);
300+
} else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) {
301+
console.error(
302+
'React Context Providers cannot be passed to Server Functions from the Client.%s',
303+
describeObjectForErrorMessage(parent, key),
304+
);
305+
} else if (objectName(value) !== 'Object') {
306+
console.error(
307+
'Only plain objects can be passed to Client Components from Server Components. ' +
308+
'%s objects are not supported.%s',
309+
objectName(value),
310+
describeObjectForErrorMessage(parent, key),
311+
);
312+
} else if (!isSimpleObject(value)) {
313+
console.error(
314+
'Only plain objects can be passed to Client Components from Server Components. ' +
315+
'Classes or other objects with methods are not supported.%s',
316+
describeObjectForErrorMessage(parent, key),
317+
);
318+
} else if (Object.getOwnPropertySymbols) {
319+
const symbols = Object.getOwnPropertySymbols(value);
320+
if (symbols.length > 0) {
299321
console.error(
300322
'Only plain objects can be passed to Client Components from Server Components. ' +
301-
'Classes or other objects with methods are not supported.%s',
323+
'Objects with symbol properties like %s are not supported.%s',
324+
symbols[0].description,
302325
describeObjectForErrorMessage(parent, key),
303326
);
304-
} else if (Object.getOwnPropertySymbols) {
305-
const symbols = Object.getOwnPropertySymbols(value);
306-
if (symbols.length > 0) {
307-
console.error(
308-
'Only plain objects can be passed to Client Components from Server Components. ' +
309-
'Objects with symbol properties like %s are not supported.%s',
310-
symbols[0].description,
311-
describeObjectForErrorMessage(parent, key),
312-
);
313-
}
314327
}
315328
}
316329
}

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -988,19 +988,21 @@ describe('ReactFlight', () => {
988988
ReactNoopFlightClient.read(transport);
989989
});
990990

991-
it('should warn in DEV if a class instance is passed to a host component', () => {
991+
it('should error if a class instance is passed to a host component', () => {
992992
class Foo {
993993
method() {}
994994
}
995-
expect(() => {
996-
const transport = ReactNoopFlightServer.render(
997-
<input value={new Foo()} />,
998-
);
999-
ReactNoopFlightClient.read(transport);
1000-
}).toErrorDev(
1001-
'Only plain objects can be passed to Client Components from Server Components. ',
1002-
{withoutStack: true},
1003-
);
995+
const errors = [];
996+
ReactNoopFlightServer.render(<input value={new Foo()} />, {
997+
onError(x) {
998+
errors.push(x.message);
999+
},
1000+
});
1001+
1002+
expect(errors).toEqual([
1003+
'Only plain objects, and a few built-ins, can be passed to Client Components ' +
1004+
'from Server Components. Classes or null prototypes are not supported.',
1005+
]);
10041006
});
10051007

10061008
it('should warn in DEV if a a client reference is passed to useContext()', () => {

packages/react-server/src/ReactFlightServer.js

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,13 @@ import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
111111
import ReactSharedInternals from 'shared/ReactSharedInternals';
112112
import ReactServerSharedInternals from './ReactServerSharedInternals';
113113
import isArray from 'shared/isArray';
114+
import getPrototypeOf from 'shared/getPrototypeOf';
114115
import binaryToComparableString from 'shared/binaryToComparableString';
115116

116117
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
117118

119+
const ObjectPrototype = Object.prototype;
120+
118121
type JSONValue =
119122
| string
120123
| boolean
@@ -1046,6 +1049,11 @@ function resolveModelToJSON(
10461049
return (undefined: any);
10471050
}
10481051

1052+
if (isArray(value)) {
1053+
// $FlowFixMe[incompatible-return]
1054+
return value;
1055+
}
1056+
10491057
if (value instanceof Map) {
10501058
return serializeMap(request, value);
10511059
}
@@ -1107,39 +1115,45 @@ function resolveModelToJSON(
11071115
}
11081116
}
11091117

1110-
if (!isArray(value)) {
1111-
const iteratorFn = getIteratorFn(value);
1112-
if (iteratorFn) {
1113-
return Array.from((value: any));
1114-
}
1118+
const iteratorFn = getIteratorFn(value);
1119+
if (iteratorFn) {
1120+
return Array.from((value: any));
11151121
}
11161122

1123+
// Verify that this is a simple plain object.
1124+
const proto = getPrototypeOf(value);
1125+
if (
1126+
proto !== ObjectPrototype &&
1127+
(proto === null || getPrototypeOf(proto) !== null)
1128+
) {
1129+
throw new Error(
1130+
'Only plain objects, and a few built-ins, can be passed to Client Components ' +
1131+
'from Server Components. Classes or null prototypes are not supported.',
1132+
);
1133+
}
11171134
if (__DEV__) {
1118-
if (value !== null && !isArray(value)) {
1119-
// Verify that this is a simple plain object.
1120-
if (objectName(value) !== 'Object') {
1121-
console.error(
1122-
'Only plain objects can be passed to Client Components from Server Components. ' +
1123-
'%s objects are not supported.%s',
1124-
objectName(value),
1125-
describeObjectForErrorMessage(parent, key),
1126-
);
1127-
} else if (!isSimpleObject(value)) {
1135+
if (objectName(value) !== 'Object') {
1136+
console.error(
1137+
'Only plain objects can be passed to Client Components from Server Components. ' +
1138+
'%s objects are not supported.%s',
1139+
objectName(value),
1140+
describeObjectForErrorMessage(parent, key),
1141+
);
1142+
} else if (!isSimpleObject(value)) {
1143+
console.error(
1144+
'Only plain objects can be passed to Client Components from Server Components. ' +
1145+
'Classes or other objects with methods are not supported.%s',
1146+
describeObjectForErrorMessage(parent, key),
1147+
);
1148+
} else if (Object.getOwnPropertySymbols) {
1149+
const symbols = Object.getOwnPropertySymbols(value);
1150+
if (symbols.length > 0) {
11281151
console.error(
11291152
'Only plain objects can be passed to Client Components from Server Components. ' +
1130-
'Classes or other objects with methods are not supported.%s',
1153+
'Objects with symbol properties like %s are not supported.%s',
1154+
symbols[0].description,
11311155
describeObjectForErrorMessage(parent, key),
11321156
);
1133-
} else if (Object.getOwnPropertySymbols) {
1134-
const symbols = Object.getOwnPropertySymbols(value);
1135-
if (symbols.length > 0) {
1136-
console.error(
1137-
'Only plain objects can be passed to Client Components from Server Components. ' +
1138-
'Objects with symbol properties like %s are not supported.%s',
1139-
symbols[0].description,
1140-
describeObjectForErrorMessage(parent, key),
1141-
);
1142-
}
11431157
}
11441158
}
11451159
}

packages/react/src/ReactTaint.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import {enableTaint, enableBinaryFlight} from 'shared/ReactFeatureFlags';
1111

12+
import getPrototypeOf from 'shared/getPrototypeOf';
13+
1214
import binaryToComparableString from 'shared/binaryToComparableString';
1315

1416
import ReactServerSharedInternals from './ReactServerSharedInternals';
@@ -22,9 +24,7 @@ const {
2224
interface Reference {}
2325

2426
// This is the shared constructor of all typed arrays.
25-
const TypedArrayConstructor = Object.getPrototypeOf(
26-
Uint32Array.prototype,
27-
).constructor;
27+
const TypedArrayConstructor = getPrototypeOf(Uint32Array.prototype).constructor;
2828

2929
const defaultMessage =
3030
'A tainted value was attempted to be serialized to a Client Component or Action closure. ' +

packages/shared/ReactSerializationErrors.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import type {LazyComponent} from 'react/src/ReactLazy';
2020

2121
import isArray from 'shared/isArray';
22+
import getPrototypeOf from 'shared/getPrototypeOf';
2223

2324
// Used for DEV messages to keep track of which parent rendered some props,
2425
// in case they error.
@@ -35,7 +36,7 @@ function isObjectPrototype(object: any): boolean {
3536
}
3637
// It might be an object from a different Realm which is
3738
// still just a plain simple object.
38-
if (Object.getPrototypeOf(object)) {
39+
if (getPrototypeOf(object)) {
3940
return false;
4041
}
4142
const names = Object.getOwnPropertyNames(object);
@@ -48,7 +49,7 @@ function isObjectPrototype(object: any): boolean {
4849
}
4950

5051
export function isSimpleObject(object: any): boolean {
51-
if (!isObjectPrototype(Object.getPrototypeOf(object))) {
52+
if (!isObjectPrototype(getPrototypeOf(object))) {
5253
return false;
5354
}
5455
const names = Object.getOwnPropertyNames(object);

packages/shared/getPrototypeOf.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
const getPrototypeOf = Object.getPrototypeOf;
11+
12+
export default getPrototypeOf;

scripts/error-codes/codes.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,5 +482,7 @@
482482
"494": "taintUniqueValue cannot taint objects or functions. Try taintObjectReference instead.",
483483
"495": "Cannot taint a %s because the value is too general and not unique enough to block globally.",
484484
"496": "Only objects or functions can be passed to taintObjectReference. Try taintUniqueValue instead.",
485-
"497": "Only objects or functions can be passed to taintObjectReference."
485+
"497": "Only objects or functions can be passed to taintObjectReference.",
486+
"498": "Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.",
487+
"499": "Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported."
486488
}

0 commit comments

Comments
 (0)