Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Enforce "simple object" rule in production #27502

Merged
merged 2 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 54 additions & 41 deletions packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import {
} from 'shared/ReactSerializationErrors';

import isArray from 'shared/isArray';
import getPrototypeOf from 'shared/getPrototypeOf';

const ObjectPrototype = Object.prototype;

import {usedWithSSR} from './ReactFlightClientConfig';

Expand Down Expand Up @@ -227,6 +230,10 @@ export function processReply(
);
return serializePromiseID(promiseId);
}
if (isArray(value)) {
// $FlowFixMe[incompatible-return]
return value;
}
// TODO: Should we the Object.prototype.toString.call() to test for cross-realm objects?
if (value instanceof FormData) {
if (formData === null) {
Expand Down Expand Up @@ -263,54 +270,60 @@ export function processReply(
formData.append(formFieldPrefix + setId, partJSON);
return serializeSetID(setId);
}
if (!isArray(value)) {
const iteratorFn = getIteratorFn(value);
if (iteratorFn) {
return Array.from((value: any));
}
const iteratorFn = getIteratorFn(value);
if (iteratorFn) {
return Array.from((value: any));
}

// Verify that this is a simple plain object.
const proto = getPrototypeOf(value);
if (
proto !== ObjectPrototype &&
(proto === null || getPrototypeOf(proto) !== null)
) {
throw new Error(
'Only plain objects, and a few built-ins, can be passed to Server Actions. ' +
'Classes or null prototypes are not supported.',
);
}
if (__DEV__) {
if (value !== null && !isArray(value)) {
// Verify that this is a simple plain object.
if ((value: any).$$typeof === REACT_ELEMENT_TYPE) {
console.error(
'React Element cannot be passed to Server Functions from the Client.%s',
describeObjectForErrorMessage(parent, key),
);
} else if ((value: any).$$typeof === REACT_LAZY_TYPE) {
console.error(
'React Lazy cannot be passed to Server Functions from the Client.%s',
describeObjectForErrorMessage(parent, key),
);
} else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) {
console.error(
'React Context Providers cannot be passed to Server Functions from the Client.%s',
describeObjectForErrorMessage(parent, key),
);
} else if (objectName(value) !== 'Object') {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'%s objects are not supported.%s',
objectName(value),
describeObjectForErrorMessage(parent, key),
);
} else if (!isSimpleObject(value)) {
if ((value: any).$$typeof === REACT_ELEMENT_TYPE) {
console.error(
'React Element cannot be passed to Server Functions from the Client.%s',
describeObjectForErrorMessage(parent, key),
);
} else if ((value: any).$$typeof === REACT_LAZY_TYPE) {
console.error(
'React Lazy cannot be passed to Server Functions from the Client.%s',
describeObjectForErrorMessage(parent, key),
);
} else if ((value: any).$$typeof === REACT_PROVIDER_TYPE) {
console.error(
'React Context Providers cannot be passed to Server Functions from the Client.%s',
describeObjectForErrorMessage(parent, key),
);
} else if (objectName(value) !== 'Object') {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'%s objects are not supported.%s',
objectName(value),
describeObjectForErrorMessage(parent, key),
);
} else if (!isSimpleObject(value)) {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Classes or other objects with methods are not supported.%s',
describeObjectForErrorMessage(parent, key),
);
} else if (Object.getOwnPropertySymbols) {
const symbols = Object.getOwnPropertySymbols(value);
if (symbols.length > 0) {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Classes or other objects with methods are not supported.%s',
'Objects with symbol properties like %s are not supported.%s',
symbols[0].description,
describeObjectForErrorMessage(parent, key),
);
} else if (Object.getOwnPropertySymbols) {
const symbols = Object.getOwnPropertySymbols(value);
if (symbols.length > 0) {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with symbol properties like %s are not supported.%s',
symbols[0].description,
describeObjectForErrorMessage(parent, key),
);
}
}
}
}
Expand Down
22 changes: 12 additions & 10 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -988,19 +988,21 @@ describe('ReactFlight', () => {
ReactNoopFlightClient.read(transport);
});

it('should warn in DEV if a class instance is passed to a host component', () => {
it('should error if a class instance is passed to a host component', () => {
class Foo {
method() {}
}
expect(() => {
const transport = ReactNoopFlightServer.render(
<input value={new Foo()} />,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ',
{withoutStack: true},
);
const errors = [];
ReactNoopFlightServer.render(<input value={new Foo()} />, {
onError(x) {
errors.push(x.message);
},
});

expect(errors).toEqual([
'Only plain objects, and a few built-ins, can be passed to Client Components ' +
'from Server Components. Classes or null prototypes are not supported.',
]);
});

it('should warn in DEV if a a client reference is passed to useContext()', () => {
Expand Down
66 changes: 40 additions & 26 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,13 @@ import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import ReactServerSharedInternals from './ReactServerSharedInternals';
import isArray from 'shared/isArray';
import getPrototypeOf from 'shared/getPrototypeOf';
import binaryToComparableString from 'shared/binaryToComparableString';

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

const ObjectPrototype = Object.prototype;

type JSONValue =
| string
| boolean
Expand Down Expand Up @@ -1046,6 +1049,11 @@ function resolveModelToJSON(
return (undefined: any);
}

if (isArray(value)) {
// $FlowFixMe[incompatible-return]
return value;
}

if (value instanceof Map) {
return serializeMap(request, value);
}
Expand Down Expand Up @@ -1107,39 +1115,45 @@ function resolveModelToJSON(
}
}

if (!isArray(value)) {
const iteratorFn = getIteratorFn(value);
if (iteratorFn) {
return Array.from((value: any));
}
const iteratorFn = getIteratorFn(value);
if (iteratorFn) {
return Array.from((value: any));
}

// Verify that this is a simple plain object.
const proto = getPrototypeOf(value);
if (
proto !== ObjectPrototype &&
(proto === null || getPrototypeOf(proto) !== null)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only new change. The other is just refactoring.

Since all objects in the common case should be proto === ObjectPrototype, it's really just one extra getPrototypeOf call per object.

) {
throw new Error(
'Only plain objects, and a few built-ins, can be passed to Client Components ' +
'from Server Components. Classes or null prototypes are not supported.',
);
}
if (__DEV__) {
if (value !== null && !isArray(value)) {
// Verify that this is a simple plain object.
if (objectName(value) !== 'Object') {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'%s objects are not supported.%s',
objectName(value),
describeObjectForErrorMessage(parent, key),
);
} else if (!isSimpleObject(value)) {
if (objectName(value) !== 'Object') {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'%s objects are not supported.%s',
objectName(value),
describeObjectForErrorMessage(parent, key),
);
} else if (!isSimpleObject(value)) {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Classes or other objects with methods are not supported.%s',
describeObjectForErrorMessage(parent, key),
);
} else if (Object.getOwnPropertySymbols) {
const symbols = Object.getOwnPropertySymbols(value);
if (symbols.length > 0) {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Classes or other objects with methods are not supported.%s',
'Objects with symbol properties like %s are not supported.%s',
symbols[0].description,
describeObjectForErrorMessage(parent, key),
);
} else if (Object.getOwnPropertySymbols) {
const symbols = Object.getOwnPropertySymbols(value);
if (symbols.length > 0) {
console.error(
'Only plain objects can be passed to Client Components from Server Components. ' +
'Objects with symbol properties like %s are not supported.%s',
symbols[0].description,
describeObjectForErrorMessage(parent, key),
);
}
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/ReactTaint.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

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

import getPrototypeOf from 'shared/getPrototypeOf';

import binaryToComparableString from 'shared/binaryToComparableString';

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

// This is the shared constructor of all typed arrays.
const TypedArrayConstructor = Object.getPrototypeOf(
Uint32Array.prototype,
).constructor;
const TypedArrayConstructor = getPrototypeOf(Uint32Array.prototype).constructor;

const defaultMessage =
'A tainted value was attempted to be serialized to a Client Component or Action closure. ' +
Expand Down
5 changes: 3 additions & 2 deletions packages/shared/ReactSerializationErrors.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import type {LazyComponent} from 'react/src/ReactLazy';

import isArray from 'shared/isArray';
import getPrototypeOf from 'shared/getPrototypeOf';

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

export function isSimpleObject(object: any): boolean {
if (!isObjectPrototype(Object.getPrototypeOf(object))) {
if (!isObjectPrototype(getPrototypeOf(object))) {
return false;
}
const names = Object.getOwnPropertyNames(object);
Expand Down
12 changes: 12 additions & 0 deletions packages/shared/getPrototypeOf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

const getPrototypeOf = Object.getPrototypeOf;

export default getPrototypeOf;
4 changes: 3 additions & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -482,5 +482,7 @@
"494": "taintUniqueValue cannot taint objects or functions. Try taintObjectReference instead.",
"495": "Cannot taint a %s because the value is too general and not unique enough to block globally.",
"496": "Only objects or functions can be passed to taintObjectReference. Try taintUniqueValue instead.",
"497": "Only objects or functions can be passed to taintObjectReference."
"497": "Only objects or functions can be passed to taintObjectReference.",
"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.",
"499": "Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported."
}