From 5074fbd074ad2885765f25363bf5751725b5a92d Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 26 Sep 2023 22:37:07 -0400 Subject: [PATCH 1/9] Add Feature Flag --- packages/shared/ReactFeatureFlags.js | 2 ++ packages/shared/forks/ReactFeatureFlags.native-fb.js | 1 + packages/shared/forks/ReactFeatureFlags.native-oss.js | 1 + packages/shared/forks/ReactFeatureFlags.test-renderer.js | 1 + packages/shared/forks/ReactFeatureFlags.test-renderer.native.js | 1 + packages/shared/forks/ReactFeatureFlags.test-renderer.www.js | 1 + packages/shared/forks/ReactFeatureFlags.www.js | 1 + 7 files changed, 8 insertions(+) diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 0a70b8cbe3818..7ac900b34f297 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -86,6 +86,8 @@ export const enableFormActions = __EXPERIMENTAL__; export const enableBinaryFlight = __EXPERIMENTAL__; +export const enableTaint = __EXPERIMENTAL__; + export const enablePostpone = __EXPERIMENTAL__; export const enableTransitionTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 299fd43418410..58ad4c29566a6 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -37,6 +37,7 @@ export const enableCacheElement = true; export const enableFetchInstrumentation = false; export const enableFormActions = true; // Doesn't affect Native export const enableBinaryFlight = true; +export const enableTaint = true; export const enablePostpone = false; export const enableSchedulerDebugging = false; export const debugRenderPhaseSideEffectsForStrictMode = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 6eec17ea53046..54a95f9a2766f 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -25,6 +25,7 @@ export const enableCacheElement = false; export const enableFetchInstrumentation = false; export const enableFormActions = true; // Doesn't affect Native export const enableBinaryFlight = true; +export const enableTaint = true; export const enablePostpone = false; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 7bc3b64e0fd69..460f61b6c3921 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -25,6 +25,7 @@ export const enableCacheElement = __EXPERIMENTAL__; export const enableFetchInstrumentation = true; export const enableFormActions = true; // Doesn't affect Test Renderer export const enableBinaryFlight = true; +export const enableTaint = true; export const enablePostpone = false; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index e54353abd6f4f..a34a2c34d9d41 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -25,6 +25,7 @@ export const enableCacheElement = true; export const enableFetchInstrumentation = false; export const enableFormActions = true; // Doesn't affect Test Renderer export const enableBinaryFlight = true; +export const enableTaint = true; export const enablePostpone = false; export const disableJavaScriptURLs = false; export const disableCommentsAsDOMContainers = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index dc90c6f837103..ad281f7e0767f 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -25,6 +25,7 @@ export const enableCacheElement = true; export const enableFetchInstrumentation = false; export const enableFormActions = true; // Doesn't affect Test Renderer export const enableBinaryFlight = true; +export const enableTaint = true; export const enablePostpone = false; export const enableSchedulerDebugging = false; export const disableJavaScriptURLs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index ef2ef9d933690..9e7f03e340b4e 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -75,6 +75,7 @@ export const enableFetchInstrumentation = false; export const enableFormActions = false; export const enableBinaryFlight = true; +export const enableTaint = false; export const enablePostpone = false; From 1710e076b57a6acb326e610511ff4f6da204c6e8 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 26 Sep 2023 23:06:50 -0400 Subject: [PATCH 2/9] Add taintValue and taintShallowObject APIs --- .../react/src/ReactSharedInternalsClient.js | 7 +- .../src/ReactSharedSubset.experimental.js | 6 ++ packages/react/src/ReactTaint.js | 88 +++++++++++++++++++ packages/react/src/ReactTaintRegistry.js | 10 +++ scripts/error-codes/codes.json | 7 +- 5 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 packages/react/src/ReactTaint.js create mode 100644 packages/react/src/ReactTaintRegistry.js diff --git a/packages/react/src/ReactSharedInternalsClient.js b/packages/react/src/ReactSharedInternalsClient.js index e036841282506..12898d30f4c1f 100644 --- a/packages/react/src/ReactSharedInternalsClient.js +++ b/packages/react/src/ReactSharedInternalsClient.js @@ -11,8 +11,9 @@ import ReactCurrentBatchConfig from './ReactCurrentBatchConfig'; import ReactCurrentActQueue from './ReactCurrentActQueue'; import ReactCurrentOwner from './ReactCurrentOwner'; import ReactDebugCurrentFrame from './ReactDebugCurrentFrame'; -import {enableServerContext} from 'shared/ReactFeatureFlags'; +import {enableServerContext, enableTaint} from 'shared/ReactFeatureFlags'; import {ContextRegistry} from './ReactServerContextRegistry'; +import {TaintRegistry} from './ReactTaintRegistry'; const ReactSharedInternals = { ReactCurrentDispatcher, @@ -30,4 +31,8 @@ if (enableServerContext) { ReactSharedInternals.ContextRegistry = ContextRegistry; } +if (enableTaint) { + ReactSharedInternals.TaintRegistry = TaintRegistry; +} + export default ReactSharedInternals; diff --git a/packages/react/src/ReactSharedSubset.experimental.js b/packages/react/src/ReactSharedSubset.experimental.js index 80d50805c23b4..94d142e1c22d5 100644 --- a/packages/react/src/ReactSharedSubset.experimental.js +++ b/packages/react/src/ReactSharedSubset.experimental.js @@ -14,6 +14,12 @@ export {default as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './R export {default as __SECRET_SERVER_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './ReactServerSharedInternals'; +// These are server-only +export { + taintValue as unstable_taintValue, + taintShallowObject as unstable_taintShallowObject, +} from './ReactTaint'; + export { Children, Fragment, diff --git a/packages/react/src/ReactTaint.js b/packages/react/src/ReactTaint.js new file mode 100644 index 0000000000000..5be91cd03edbe --- /dev/null +++ b/packages/react/src/ReactTaint.js @@ -0,0 +1,88 @@ +/** + * 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 + */ + +import {enableTaint} from 'shared/ReactFeatureFlags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; + +const TaintRegistry = ReactSharedInternals.TaintRegistry; + +interface Reference {} + +const TypedArrayConstructor = Object.getPrototypeOf(Uint8Array.prototype); + +const defaultMessage = + 'A tainted value was attempted to be serialized to a Client Component or Action closure. ' + + 'This would leak it to the client.'; + +export function taintValue( + message: ?string, + lifetime: Reference, + value: string | bigint | $ArrayBufferView, +): void { + if (!enableTaint) { + throw new Error('Not implemented.'); + } + // eslint-disable-next-line react-internal/safe-string-coercion + message = '' + (message || defaultMessage); + if ( + lifetime === null || + (typeof lifetime !== 'object' && typeof lifetime !== 'function') + ) { + throw new Error( + 'To taint a value, a life time must be defined by passing an object that holds ' + + 'the value.', + ); + } + if (typeof value === 'string') { + return; + } + if (typeof value === 'bigint') { + return; + } + if (value instanceof TypedArrayConstructor) { + return; + } + if (value instanceof DataView) { + return; + } + const kind = value === null ? 'null' : typeof value; + if (kind === 'object' || kind === 'function') { + throw new Error( + 'taintValue cannot taint objects or functions. Try taintShallowObject instead.', + ); + } + throw new Error( + 'Cannot taint a ' + + kind + + ' because the value is too general and cannot be ' + + 'a secret by', + ); +} + +export function taintShallowObject(message: ?string, object: Reference): void { + if (!enableTaint) { + throw new Error('Not implemented.'); + } + // eslint-disable-next-line react-internal/safe-string-coercion + message = '' + (message || defaultMessage); + if (typeof object === 'string' || typeof object === 'bigint') { + throw new Error( + 'Only objects or functions can be passed to taintShallowObject. Try taintValue instead.', + ); + } + if ( + object === null || + (typeof object !== 'object' && typeof object !== 'function') + ) { + throw new Error( + 'Only objects or functions can be passed to taintShallowObject.', + ); + } + // TODO +} diff --git a/packages/react/src/ReactTaintRegistry.js b/packages/react/src/ReactTaintRegistry.js new file mode 100644 index 0000000000000..3eed75032fa67 --- /dev/null +++ b/packages/react/src/ReactTaintRegistry.js @@ -0,0 +1,10 @@ +/** + * 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 + */ + +export const TaintRegistry: {} = {}; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 43d448ca977b4..373bc8762c68c 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -477,5 +477,10 @@ "489": "Expected to see a component of type \"%s\" in this slot. The tree doesn't match so React will fallback to client rendering.", "490": "Expected to see a Suspense boundary in this slot. The tree doesn't match so React will fallback to client rendering.", "491": "It should not be possible to postpone both at the root of an element as well as a slot below. This is a bug in React.", - "492": "The \"react\" package in this environment is not configured correctly. The \"react-server\" condition must be enabled in any environment that runs React Server Components." + "492": "The \"react\" package in this environment is not configured correctly. The \"react-server\" condition must be enabled in any environment that runs React Server Components.", + "493": "To taint a value, a life time must be defined by passing an object that holds the value.", + "494": "taintValue cannot taint objects or functions. Try taintShallowObject instead.", + "495": "Cannot taint a %s because the value is too general and cannot be a secret by", + "496": "Only objects or functions can be passed to taintShallowObject. Try taintValue instead.", + "497": "Only objects or functions can be passed to taintShallowObject." } \ No newline at end of file From d72224345ec1334838cea83ee4c4cd0e48374a83 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 27 Sep 2023 23:01:32 -0400 Subject: [PATCH 3/9] Register tainted values with the registry Clean up values when the life time object gets finalized. --- .eslintrc.js | 1 + .../react/src/ReactServerSharedInternals.js | 8 ++ .../react/src/ReactSharedInternalsClient.js | 7 +- packages/react/src/ReactTaint.js | 88 +++++++++++++------ packages/react/src/ReactTaintRegistry.js | 10 ++- packages/shared/binaryToComparableString.js | 19 ++++ scripts/flow/environment.js | 2 + scripts/rollup/validate/eslintrc.cjs.js | 3 + scripts/rollup/validate/eslintrc.cjs2015.js | 1 + scripts/rollup/validate/eslintrc.esm.js | 3 + scripts/rollup/validate/eslintrc.fb.js | 3 + scripts/rollup/validate/eslintrc.rn.js | 3 + scripts/rollup/validate/eslintrc.umd.js | 3 + 13 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 packages/shared/binaryToComparableString.js diff --git a/.eslintrc.js b/.eslintrc.js index a00174fea7122..3337fb94933c4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -506,6 +506,7 @@ module.exports = { Thenable: 'readonly', TimeoutID: 'readonly', WheelEventHandler: 'readonly', + FinalizationRegistry: 'readonly', spyOnDev: 'readonly', spyOnDevAndProd: 'readonly', diff --git a/packages/react/src/ReactServerSharedInternals.js b/packages/react/src/ReactServerSharedInternals.js index 3e9b81f4ec149..c3d7f72e0056c 100644 --- a/packages/react/src/ReactServerSharedInternals.js +++ b/packages/react/src/ReactServerSharedInternals.js @@ -7,10 +7,18 @@ import ReactCurrentDispatcher from './ReactCurrentDispatcher'; import ReactCurrentCache from './ReactCurrentCache'; +import {TaintRegistryObjects, TaintRegistryValues} from './ReactTaintRegistry'; + +import {enableTaint} from 'shared/ReactFeatureFlags'; const ReactServerSharedInternals = { ReactCurrentDispatcher, ReactCurrentCache, }; +if (enableTaint) { + ReactServerSharedInternals.TaintRegistryObjects = TaintRegistryObjects; + ReactServerSharedInternals.TaintRegistryValues = TaintRegistryValues; +} + export default ReactServerSharedInternals; diff --git a/packages/react/src/ReactSharedInternalsClient.js b/packages/react/src/ReactSharedInternalsClient.js index 12898d30f4c1f..e036841282506 100644 --- a/packages/react/src/ReactSharedInternalsClient.js +++ b/packages/react/src/ReactSharedInternalsClient.js @@ -11,9 +11,8 @@ import ReactCurrentBatchConfig from './ReactCurrentBatchConfig'; import ReactCurrentActQueue from './ReactCurrentActQueue'; import ReactCurrentOwner from './ReactCurrentOwner'; import ReactDebugCurrentFrame from './ReactDebugCurrentFrame'; -import {enableServerContext, enableTaint} from 'shared/ReactFeatureFlags'; +import {enableServerContext} from 'shared/ReactFeatureFlags'; import {ContextRegistry} from './ReactServerContextRegistry'; -import {TaintRegistry} from './ReactTaintRegistry'; const ReactSharedInternals = { ReactCurrentDispatcher, @@ -31,8 +30,4 @@ if (enableServerContext) { ReactSharedInternals.ContextRegistry = ContextRegistry; } -if (enableTaint) { - ReactSharedInternals.TaintRegistry = TaintRegistry; -} - export default ReactSharedInternals; diff --git a/packages/react/src/ReactTaint.js b/packages/react/src/ReactTaint.js index 5be91cd03edbe..5c2c6acf2890e 100644 --- a/packages/react/src/ReactTaint.js +++ b/packages/react/src/ReactTaint.js @@ -8,18 +8,41 @@ */ import {enableTaint} from 'shared/ReactFeatureFlags'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; -const TaintRegistry = ReactSharedInternals.TaintRegistry; +import binaryToComparableString from 'shared/binaryToComparableString'; + +import ReactServerSharedInternals from './ReactServerSharedInternals'; +const {TaintRegistryObjects, TaintRegistryValues} = ReactServerSharedInternals; interface Reference {} -const TypedArrayConstructor = Object.getPrototypeOf(Uint8Array.prototype); +// This is the shared constructor of all typed arrays. +const TypedArrayConstructor = Object.getPrototypeOf( + Uint32Array.prototype, +).constructor; const defaultMessage = 'A tainted value was attempted to be serialized to a Client Component or Action closure. ' + 'This would leak it to the client.'; +function cleanup(entryValue: string | bigint): void { + const entry = TaintRegistryValues.get(entryValue); + if (entry !== undefined) { + if (entry.count === 1) { + TaintRegistryValues.delete(entryValue); + } else { + entry.count--; + } + } +} + +// If FinalizationRegistry doesn't exist, we assume that objects life forever. +// E.g. the whole VM is just the lifetime of a request. +const finalizationRegistry = + typeof FinalizationRegistry === 'function' + ? new FinalizationRegistry(cleanup) + : null; + export function taintValue( message: ?string, lifetime: Reference, @@ -39,30 +62,45 @@ export function taintValue( 'the value.', ); } - if (typeof value === 'string') { - return; - } - if (typeof value === 'bigint') { - return; - } - if (value instanceof TypedArrayConstructor) { - return; - } - if (value instanceof DataView) { - return; - } - const kind = value === null ? 'null' : typeof value; - if (kind === 'object' || kind === 'function') { + let entryValue: string | bigint; + if (typeof value === 'string' || typeof value === 'bigint') { + // Use as is. + entryValue = value; + } else if ( + value instanceof TypedArrayConstructor || + value instanceof DataView + ) { + // For now, we just convert binary data to a string so that we can just use the native + // hashing in the Map implementation. It doesn't really matter what form the string + // take as long as it's the same when we look it up. + // We're not too worried about collisions since this should be a high entropy value. + entryValue = binaryToComparableString(value); + } else { + const kind = value === null ? 'null' : typeof value; + if (kind === 'object' || kind === 'function') { + throw new Error( + 'taintValue cannot taint objects or functions. Try taintShallowObject instead.', + ); + } throw new Error( - 'taintValue cannot taint objects or functions. Try taintShallowObject instead.', + 'Cannot taint a ' + + kind + + ' because the value is too general and cannot be ' + + 'a secret by', ); } - throw new Error( - 'Cannot taint a ' + - kind + - ' because the value is too general and cannot be ' + - 'a secret by', - ); + const existingEntry = TaintRegistryValues.get(entryValue); + if (existingEntry === undefined) { + TaintRegistryValues.set(entryValue, { + message, + count: 1, + }); + } else { + existingEntry.count++; + } + if (finalizationRegistry !== null) { + finalizationRegistry.register(lifetime, entryValue); + } } export function taintShallowObject(message: ?string, object: Reference): void { @@ -84,5 +122,5 @@ export function taintShallowObject(message: ?string, object: Reference): void { 'Only objects or functions can be passed to taintShallowObject.', ); } - // TODO + TaintRegistryObjects.set(object, message); } diff --git a/packages/react/src/ReactTaintRegistry.js b/packages/react/src/ReactTaintRegistry.js index 3eed75032fa67..b60947dc6617c 100644 --- a/packages/react/src/ReactTaintRegistry.js +++ b/packages/react/src/ReactTaintRegistry.js @@ -7,4 +7,12 @@ * @flow */ -export const TaintRegistry: {} = {}; +interface Reference {} + +type TaintEntry = { + message: string, + count: number, +}; + +export const TaintRegistryObjects: WeakMap = new WeakMap(); +export const TaintRegistryValues: Map = new Map(); diff --git a/packages/shared/binaryToComparableString.js b/packages/shared/binaryToComparableString.js new file mode 100644 index 0000000000000..731142095467f --- /dev/null +++ b/packages/shared/binaryToComparableString.js @@ -0,0 +1,19 @@ +/** + * 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 + */ + +// Turns a TypedArray or ArrayBuffer into a string that can be used for comparison +// in a Map to see if the bytes are the same. +export default function binaryToComparableString( + view: $ArrayBufferView, +): string { + return String.fromCharCode.apply( + String, + new Uint8Array(view.buffer, view.byteOffset, view.byteLength), + ); +} diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index a175f4f5910cf..a65433dc533e0 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -24,6 +24,8 @@ declare var queueMicrotask: (fn: Function) => void; declare var reportError: (error: mixed) => void; declare var AggregateError: Class; +declare var FinalizationRegistry: any; + declare module 'create-react-class' { declare var exports: React$CreateClass; } diff --git a/scripts/rollup/validate/eslintrc.cjs.js b/scripts/rollup/validate/eslintrc.cjs.js index 63406a6a6b245..7bb549cc6087e 100644 --- a/scripts/rollup/validate/eslintrc.cjs.js +++ b/scripts/rollup/validate/eslintrc.cjs.js @@ -31,6 +31,9 @@ module.exports = { Reflect: 'readonly', globalThis: 'readonly', + + FinalizationRegistry: 'readonly', + // Vendor specific MSApp: 'readonly', __REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly', diff --git a/scripts/rollup/validate/eslintrc.cjs2015.js b/scripts/rollup/validate/eslintrc.cjs2015.js index 3c8ade7946c71..4f00ddc6f1e04 100644 --- a/scripts/rollup/validate/eslintrc.cjs2015.js +++ b/scripts/rollup/validate/eslintrc.cjs2015.js @@ -31,6 +31,7 @@ module.exports = { Reflect: 'readonly', globalThis: 'readonly', + FinalizationRegistry: 'readonly', // Vendor specific MSApp: 'readonly', __REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly', diff --git a/scripts/rollup/validate/eslintrc.esm.js b/scripts/rollup/validate/eslintrc.esm.js index a46004a25bed1..0de98a0c7ce20 100644 --- a/scripts/rollup/validate/eslintrc.esm.js +++ b/scripts/rollup/validate/eslintrc.esm.js @@ -31,6 +31,9 @@ module.exports = { Reflect: 'readonly', globalThis: 'readonly', + + FinalizationRegistry: 'readonly', + // Vendor specific MSApp: 'readonly', __REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly', diff --git a/scripts/rollup/validate/eslintrc.fb.js b/scripts/rollup/validate/eslintrc.fb.js index 94483e5fe075a..ef6ed5d43674c 100644 --- a/scripts/rollup/validate/eslintrc.fb.js +++ b/scripts/rollup/validate/eslintrc.fb.js @@ -31,6 +31,9 @@ module.exports = { Reflect: 'readonly', globalThis: 'readonly', + + FinalizationRegistry: 'readonly', + // Vendor specific MSApp: 'readonly', __REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly', diff --git a/scripts/rollup/validate/eslintrc.rn.js b/scripts/rollup/validate/eslintrc.rn.js index 9038701285521..efc2a62a098c6 100644 --- a/scripts/rollup/validate/eslintrc.rn.js +++ b/scripts/rollup/validate/eslintrc.rn.js @@ -31,6 +31,9 @@ module.exports = { Reflect: 'readonly', globalThis: 'readonly', + + FinalizationRegistry: 'readonly', + // Vendor specific MSApp: 'readonly', __REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly', diff --git a/scripts/rollup/validate/eslintrc.umd.js b/scripts/rollup/validate/eslintrc.umd.js index 3d35c688bdbf0..a8e6de45a22d7 100644 --- a/scripts/rollup/validate/eslintrc.umd.js +++ b/scripts/rollup/validate/eslintrc.umd.js @@ -30,6 +30,9 @@ module.exports = { Reflect: 'readonly', globalThis: 'readonly', + + FinalizationRegistry: 'readonly', + // Vendor specific MSApp: 'readonly', __REACT_DEVTOOLS_GLOBAL_HOOK__: 'readonly', From c34003db1b7c67675336fab55524aba5dddb1016 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 27 Sep 2023 23:15:41 -0400 Subject: [PATCH 4/9] Check the registry before serializing --- .../react-server/src/ReactFlightServer.js | 57 +++++++++++++++++-- packages/react/src/ReactTaint.js | 6 +- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 230994011fb93..c6b14686b389a 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -11,7 +11,11 @@ import type {Chunk, BinaryChunk, Destination} from './ReactServerStreamConfig'; import type {Postpone} from 'react/src/ReactPostpone'; -import {enableBinaryFlight, enablePostpone} from 'shared/ReactFeatureFlags'; +import { + enableBinaryFlight, + enablePostpone, + enableTaint, +} from 'shared/ReactFeatureFlags'; import { scheduleWork, @@ -106,6 +110,8 @@ import { import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; import ReactServerSharedInternals from './ReactServerSharedInternals'; import isArray from 'shared/isArray'; +import binaryToComparableString from 'shared/binaryToComparableString'; + import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable'; type JSONValue = @@ -197,9 +203,17 @@ export type Request = { toJSON: (key: string, value: ReactClientValue) => ReactJSONValue, }; -const ReactCurrentDispatcher = - ReactServerSharedInternals.ReactCurrentDispatcher; -const ReactCurrentCache = ReactServerSharedInternals.ReactCurrentCache; +const { + TaintRegistryObjects, + TaintRegistryValues, + ReactCurrentDispatcher, + ReactCurrentCache, +} = ReactServerSharedInternals; + +function throwTaintViolation(message: string) { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error(message); +} function defaultErrorHandler(error: mixed) { console['error'](error); @@ -781,6 +795,17 @@ function serializeTypedArray( tag: string, typedArray: $ArrayBufferView, ): string { + if (enableTaint) { + // TODO: This is way too slow for the common case. We should first check if + // there even could be a match such as if there are any tainted arrays of + // equal size. + const tainted = TaintRegistryValues.get( + binaryToComparableString(typedArray), + ); + if (tainted !== undefined) { + throwTaintViolation(tainted.message); + } + } request.pendingChunks += 2; const bufferId = request.nextChunkId++; // TODO: Convert to little endian if that's not the server default. @@ -959,6 +984,12 @@ function resolveModelToJSON( } if (typeof value === 'object') { + if (enableTaint) { + const tainted = TaintRegistryObjects.get(value); + if (tainted !== undefined) { + throwTaintViolation(tainted); + } + } if (isClientReference(value)) { return serializeClientReference(request, parent, key, (value: any)); // $FlowFixMe[method-unbinding] @@ -1091,6 +1122,12 @@ function resolveModelToJSON( } if (typeof value === 'string') { + if (enableTaint) { + const tainted = TaintRegistryValues.get(value); + if (tainted !== undefined) { + throwTaintViolation(tainted.message); + } + } // TODO: Maybe too clever. If we support URL there's no similar trick. if (value[value.length - 1] === 'Z') { // Possibly a Date, whose toJSON automatically calls toISOString @@ -1122,6 +1159,12 @@ function resolveModelToJSON( } if (typeof value === 'function') { + if (enableTaint) { + const tainted = TaintRegistryObjects.get(value); + if (tainted !== undefined) { + throwTaintViolation(tainted); + } + } if (isClientReference(value)) { return serializeClientReference(request, parent, key, (value: any)); } @@ -1171,6 +1214,12 @@ function resolveModelToJSON( } if (typeof value === 'bigint') { + if (enableTaint) { + const tainted = TaintRegistryValues.get(value); + if (tainted !== undefined) { + throwTaintViolation(tainted.message); + } + } return serializeBigInt(value); } diff --git a/packages/react/src/ReactTaint.js b/packages/react/src/ReactTaint.js index 5c2c6acf2890e..636404dad8e19 100644 --- a/packages/react/src/ReactTaint.js +++ b/packages/react/src/ReactTaint.js @@ -7,7 +7,7 @@ * @flow */ -import {enableTaint} from 'shared/ReactFeatureFlags'; +import {enableTaint, enableBinaryFlight} from 'shared/ReactFeatureFlags'; import binaryToComparableString from 'shared/binaryToComparableString'; @@ -67,8 +67,8 @@ export function taintValue( // Use as is. entryValue = value; } else if ( - value instanceof TypedArrayConstructor || - value instanceof DataView + enableBinaryFlight && + (value instanceof TypedArrayConstructor || value instanceof DataView) ) { // For now, we just convert binary data to a string so that we can just use the native // hashing in the Map implementation. It doesn't really matter what form the string From a37bd86f7a50751a7928d9b7fd65d3e48beb1686 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 29 Sep 2023 16:34:00 -0400 Subject: [PATCH 5/9] Optimize binary value checking Instead of copying all typed arrays that we serialize into a string, we only do that if the length of the data is the length of any tainted binary value first. You're not really supposed to use this with arbitrary binary data but mostly things like hashes or tokens that end up with the same length. --- packages/react-server/src/ReactFlightServer.js | 18 ++++++++++-------- .../react/src/ReactServerSharedInternals.js | 8 +++++++- packages/react/src/ReactTaint.js | 4 +++- packages/react/src/ReactTaintRegistry.js | 3 +++ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index c6b14686b389a..b92ecbe40bfbe 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -206,6 +206,7 @@ export type Request = { const { TaintRegistryObjects, TaintRegistryValues, + TaintRegistryByteLengths, ReactCurrentDispatcher, ReactCurrentCache, } = ReactServerSharedInternals; @@ -796,14 +797,15 @@ function serializeTypedArray( typedArray: $ArrayBufferView, ): string { if (enableTaint) { - // TODO: This is way too slow for the common case. We should first check if - // there even could be a match such as if there are any tainted arrays of - // equal size. - const tainted = TaintRegistryValues.get( - binaryToComparableString(typedArray), - ); - if (tainted !== undefined) { - throwTaintViolation(tainted.message); + if (TaintRegistryByteLengths.has(typedArray.byteLength)) { + // If we have had any tainted values of this length, we check + // to see if these bytes matches any entries in the registry. + const tainted = TaintRegistryValues.get( + binaryToComparableString(typedArray), + ); + if (tainted !== undefined) { + throwTaintViolation(tainted.message); + } } } request.pendingChunks += 2; diff --git a/packages/react/src/ReactServerSharedInternals.js b/packages/react/src/ReactServerSharedInternals.js index c3d7f72e0056c..2877bdd6f7de3 100644 --- a/packages/react/src/ReactServerSharedInternals.js +++ b/packages/react/src/ReactServerSharedInternals.js @@ -7,7 +7,11 @@ import ReactCurrentDispatcher from './ReactCurrentDispatcher'; import ReactCurrentCache from './ReactCurrentCache'; -import {TaintRegistryObjects, TaintRegistryValues} from './ReactTaintRegistry'; +import { + TaintRegistryObjects, + TaintRegistryValues, + TaintRegistryByteLengths, +} from './ReactTaintRegistry'; import {enableTaint} from 'shared/ReactFeatureFlags'; @@ -19,6 +23,8 @@ const ReactServerSharedInternals = { if (enableTaint) { ReactServerSharedInternals.TaintRegistryObjects = TaintRegistryObjects; ReactServerSharedInternals.TaintRegistryValues = TaintRegistryValues; + ReactServerSharedInternals.TaintRegistryByteLengths = + TaintRegistryByteLengths; } export default ReactServerSharedInternals; diff --git a/packages/react/src/ReactTaint.js b/packages/react/src/ReactTaint.js index 636404dad8e19..63e9ce7f98395 100644 --- a/packages/react/src/ReactTaint.js +++ b/packages/react/src/ReactTaint.js @@ -12,7 +12,8 @@ import {enableTaint, enableBinaryFlight} from 'shared/ReactFeatureFlags'; import binaryToComparableString from 'shared/binaryToComparableString'; import ReactServerSharedInternals from './ReactServerSharedInternals'; -const {TaintRegistryObjects, TaintRegistryValues} = ReactServerSharedInternals; +const {TaintRegistryObjects, TaintRegistryValues, TaintRegistryByteLengths} = + ReactServerSharedInternals; interface Reference {} @@ -74,6 +75,7 @@ export function taintValue( // hashing in the Map implementation. It doesn't really matter what form the string // take as long as it's the same when we look it up. // We're not too worried about collisions since this should be a high entropy value. + TaintRegistryByteLengths.add(value.byteLength); entryValue = binaryToComparableString(value); } else { const kind = value === null ? 'null' : typeof value; diff --git a/packages/react/src/ReactTaintRegistry.js b/packages/react/src/ReactTaintRegistry.js index b60947dc6617c..8db64d13965b8 100644 --- a/packages/react/src/ReactTaintRegistry.js +++ b/packages/react/src/ReactTaintRegistry.js @@ -16,3 +16,6 @@ type TaintEntry = { export const TaintRegistryObjects: WeakMap = new WeakMap(); export const TaintRegistryValues: Map = new Map(); +// Byte lengths of all binary values we've ever seen. We don't both refcounting this. +// We expect to see only a few lengths here such as the length of token. +export const TaintRegistryByteLengths: Set = new Set(); From 8d914acaeedbd7452012ea4699c297478e72a451 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 28 Sep 2023 19:03:58 -0400 Subject: [PATCH 6/9] Test --- .../src/__tests__/ReactFlight-test.js | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 30cfda19af6ec..e5f84fce5ea9b 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1446,4 +1446,148 @@ describe('ReactFlight', () => { ); }); }); + + // @gate enableTaint + it('errors when a tainted object is serialized', async () => { + function UserClient({user}) { + return {user.name}; + } + const User = clientReference(UserClient); + + const user = { + name: 'Seb', + age: 'rather not say', + }; + ReactServer.unstable_taintShallowObject( + "Don't pass the raw user object to the client", + user, + ); + const errors = []; + ReactNoopFlightServer.render(, { + onError(x) { + errors.push(x.message); + }, + }); + + expect(errors).toEqual(["Don't pass the raw user object to the client"]); + }); + + // @gate enableTaint + it('errors with a specific message when a tainted function is serialized', async () => { + function UserClient({user}) { + return {user.name}; + } + const User = clientReference(UserClient); + + function change() {} + ReactServer.unstable_taintShallowObject( + 'A change handler cannot be passed to a client component', + change, + ); + const errors = []; + ReactNoopFlightServer.render(, { + onError(x) { + errors.push(x.message); + }, + }); + + expect(errors).toEqual([ + 'A change handler cannot be passed to a client component', + ]); + }); + + // @gate enableTaint + it('errors when a tainted string is serialized', async () => { + function UserClient({user}) { + return {user.name}; + } + const User = clientReference(UserClient); + + const process = { + env: { + SECRET: '3e971ecc1485fe78625598bf9b6f85db', + }, + }; + ReactServer.unstable_taintValue( + 'Cannot pass a secret token to the client', + process, + process.env.SECRET, + ); + + const errors = []; + ReactNoopFlightServer.render(, { + onError(x) { + errors.push(x.message); + }, + }); + + expect(errors).toEqual(['Cannot pass a secret token to the client']); + + // This just ensures the process object is kept alive for the life time of + // the test since we're simulating a global as an example. + expect(process.env.SECRET).toBe('3e971ecc1485fe78625598bf9b6f85db'); + }); + + // @gate enableTaint + it('errors when a tainted bigint is serialized', async () => { + function UserClient({user}) { + return {user.name}; + } + const User = clientReference(UserClient); + + const currentUser = { + name: 'Seb', + token: BigInt('0x3e971ecc1485fe78625598bf9b6f85dc'), + }; + ReactServer.unstable_taintValue( + 'Cannot pass a secret token to the client', + currentUser, + currentUser.token, + ); + + function App({user}) { + return ; + } + + const errors = []; + ReactNoopFlightServer.render(, { + onError(x) { + errors.push(x.message); + }, + }); + + expect(errors).toEqual(['Cannot pass a secret token to the client']); + }); + + // @gate enableTaint && enableBinaryFlight + it('errors when a tainted binary value is serialized', async () => { + function UserClient({user}) { + return {user.name}; + } + const User = clientReference(UserClient); + + const currentUser = { + name: 'Seb', + token: new Uint32Array([0x3e971ecc, 0x1485fe78, 0x625598bf, 0x9b6f85dd]), + }; + ReactServer.unstable_taintValue( + 'Cannot pass a secret token to the client', + currentUser, + currentUser.token, + ); + + function App({user}) { + const clone = user.token.slice(); + return ; + } + + const errors = []; + ReactNoopFlightServer.render(, { + onError(x) { + errors.push(x.message); + }, + }); + + expect(errors).toEqual(['Cannot pass a secret token to the client']); + }); }); From d4f1c3f378fb750ba867b000c8b139af5a08aaa2 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 29 Sep 2023 16:53:58 -0400 Subject: [PATCH 7/9] Keep values alive until the end of any pending requests A request is the only thing that is expected to do any work. The principle is that you can derive values from out of a tainted entry during a request. Including stashing it in a per request cache. What you can't do is store a derived value in a global module level cache. At least not without also tainting the object. --- .../src/__tests__/ReactFlight-test.js | 71 +++++++++++++++++++ .../react-server/src/ReactFlightServer.js | 30 ++++++++ .../react/src/ReactServerSharedInternals.js | 3 + packages/react/src/ReactTaint.js | 12 +++- packages/react/src/ReactTaintRegistry.js | 6 ++ 5 files changed, 120 insertions(+), 2 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index e5f84fce5ea9b..229b4ed48d78f 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -10,6 +10,23 @@ 'use strict'; +const heldValues = []; +let finalizationCallback; +function FinalizationRegistryMock(callback) { + finalizationCallback = callback; +} +FinalizationRegistryMock.prototype.register = function (target, heldValue) { + heldValues.push(heldValue); +}; +global.FinalizationRegistry = FinalizationRegistryMock; + +function gc() { + for (let i = 0; i < heldValues.length; i++) { + finalizationCallback(heldValues[i]); + } + heldValues.length = 0; +} + let act; let use; let startTransition; @@ -1590,4 +1607,58 @@ describe('ReactFlight', () => { expect(errors).toEqual(['Cannot pass a secret token to the client']); }); + + // @gate enableTaint + it('keep a tainted value tainted until the end of any pending requests', async () => { + function UserClient({user}) { + return {user.name}; + } + const User = clientReference(UserClient); + + function getUser() { + const user = { + name: 'Seb', + token: '3e971ecc1485fe78625598bf9b6f85db', + }; + ReactServer.unstable_taintValue( + 'Cannot pass a secret token to the client', + user, + user.token, + ); + return user; + } + + function App() { + const user = getUser(); + const derivedValue = {...user}; + // A garbage collection can happen at any time. Even before the end of + // this request. This would clean up the user object. + gc(); + // We should still block the tainted value. + return ; + } + + let errors = []; + ReactNoopFlightServer.render(, { + onError(x) { + errors.push(x.message); + }, + }); + + expect(errors).toEqual(['Cannot pass a secret token to the client']); + + // After the previous requests finishes, the token can be rendered again. + + errors = []; + ReactNoopFlightServer.render( + , + { + onError(x) { + errors.push(x.message); + }, + }, + ); + + expect(errors).toEqual([]); + }); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index b92ecbe40bfbe..c4b26520b6016 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -198,6 +198,7 @@ export type Request = { writtenProviders: Map, identifierPrefix: string, identifierCount: number, + taintCleanupQueue: Array, onError: (error: mixed) => ?string, onPostpone: (reason: string) => void, toJSON: (key: string, value: ReactClientValue) => ReactJSONValue, @@ -207,6 +208,7 @@ const { TaintRegistryObjects, TaintRegistryValues, TaintRegistryByteLengths, + TaintRegistryPendingRequests, ReactCurrentDispatcher, ReactCurrentCache, } = ReactServerSharedInternals; @@ -216,6 +218,23 @@ function throwTaintViolation(message: string) { throw new Error(message); } +function cleanupTaintQueue(request: Request): void { + const cleanupQueue = request.taintCleanupQueue; + TaintRegistryPendingRequests.delete(cleanupQueue); + for (let i = 0; i < cleanupQueue.length; i++) { + const entryValue = cleanupQueue[i]; + const entry = TaintRegistryValues.get(entryValue); + if (entry !== undefined) { + if (entry.count === 1) { + TaintRegistryValues.delete(entryValue); + } else { + entry.count--; + } + } + } + cleanupQueue.length = 0; +} + function defaultErrorHandler(error: mixed) { console['error'](error); // Don't transform to our wrapper @@ -250,6 +269,10 @@ export function createRequest( const abortSet: Set = new Set(); const pingedTasks: Array = []; + const cleanupQueue: Array = []; + if (enableTaint) { + TaintRegistryPendingRequests.add(cleanupQueue); + } const hints = createHints(); const request: Request = { status: OPEN, @@ -273,6 +296,7 @@ export function createRequest( writtenProviders: new Map(), identifierPrefix: identifierPrefix || '', identifierCount: 1, + taintCleanupQueue: cleanupQueue, onError: onError === undefined ? defaultErrorHandler : onError, onPostpone: onPostpone === undefined ? defaultPostponeHandler : onPostpone, // $FlowFixMe[missing-this-annot] @@ -1249,6 +1273,9 @@ function logRecoverableError(request: Request, error: mixed): string { } function fatalError(request: Request, error: mixed): void { + if (enableTaint) { + cleanupTaintQueue(request); + } // This is called outside error handling code such as if an error happens in React internals. if (request.destination !== null) { request.status = CLOSED; @@ -1573,6 +1600,9 @@ function flushCompletedChunks( flushBuffered(destination); if (request.pendingChunks === 0) { // We're done. + if (enableTaint) { + cleanupTaintQueue(request); + } close(destination); } } diff --git a/packages/react/src/ReactServerSharedInternals.js b/packages/react/src/ReactServerSharedInternals.js index 2877bdd6f7de3..7b0cef8cb0a01 100644 --- a/packages/react/src/ReactServerSharedInternals.js +++ b/packages/react/src/ReactServerSharedInternals.js @@ -11,6 +11,7 @@ import { TaintRegistryObjects, TaintRegistryValues, TaintRegistryByteLengths, + TaintRegistryPendingRequests, } from './ReactTaintRegistry'; import {enableTaint} from 'shared/ReactFeatureFlags'; @@ -25,6 +26,8 @@ if (enableTaint) { ReactServerSharedInternals.TaintRegistryValues = TaintRegistryValues; ReactServerSharedInternals.TaintRegistryByteLengths = TaintRegistryByteLengths; + ReactServerSharedInternals.TaintRegistryPendingRequests = + TaintRegistryPendingRequests; } export default ReactServerSharedInternals; diff --git a/packages/react/src/ReactTaint.js b/packages/react/src/ReactTaint.js index 63e9ce7f98395..cc54f89b17688 100644 --- a/packages/react/src/ReactTaint.js +++ b/packages/react/src/ReactTaint.js @@ -12,8 +12,12 @@ import {enableTaint, enableBinaryFlight} from 'shared/ReactFeatureFlags'; import binaryToComparableString from 'shared/binaryToComparableString'; import ReactServerSharedInternals from './ReactServerSharedInternals'; -const {TaintRegistryObjects, TaintRegistryValues, TaintRegistryByteLengths} = - ReactServerSharedInternals; +const { + TaintRegistryObjects, + TaintRegistryValues, + TaintRegistryByteLengths, + TaintRegistryPendingRequests, +} = ReactServerSharedInternals; interface Reference {} @@ -29,6 +33,10 @@ const defaultMessage = function cleanup(entryValue: string | bigint): void { const entry = TaintRegistryValues.get(entryValue); if (entry !== undefined) { + TaintRegistryPendingRequests.forEach(function (requestQueue) { + requestQueue.push(entryValue); + entry.count++; + }); if (entry.count === 1) { TaintRegistryValues.delete(entryValue); } else { diff --git a/packages/react/src/ReactTaintRegistry.js b/packages/react/src/ReactTaintRegistry.js index 8db64d13965b8..d600e640b523c 100644 --- a/packages/react/src/ReactTaintRegistry.js +++ b/packages/react/src/ReactTaintRegistry.js @@ -19,3 +19,9 @@ export const TaintRegistryValues: Map = new Map(); // Byte lengths of all binary values we've ever seen. We don't both refcounting this. // We expect to see only a few lengths here such as the length of token. export const TaintRegistryByteLengths: Set = new Set(); + +// When a value is finalized, it means that it has been removed from any global caches. +// No future requests can get a handle on it but any ongoing requests can still have +// a handle on it. It's still tainted until that happens. +type RequestCleanupQueue = Array; +export const TaintRegistryPendingRequests: Set = new Set(); From 4f2c039b7b735e730138dbd5096803a8aabfa9b1 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 1 Oct 2023 23:01:01 -0400 Subject: [PATCH 8/9] Rename APIs --- .../react-client/src/__tests__/ReactFlight-test.js | 12 ++++++------ .../react/src/ReactSharedSubset.experimental.js | 4 ++-- packages/react/src/ReactTaint.js | 13 ++++++++----- scripts/error-codes/codes.json | 6 +++--- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 229b4ed48d78f..c0f3d2bbb0b04 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1475,7 +1475,7 @@ describe('ReactFlight', () => { name: 'Seb', age: 'rather not say', }; - ReactServer.unstable_taintShallowObject( + ReactServer.experimental_taintObjectReference( "Don't pass the raw user object to the client", user, ); @@ -1497,7 +1497,7 @@ describe('ReactFlight', () => { const User = clientReference(UserClient); function change() {} - ReactServer.unstable_taintShallowObject( + ReactServer.experimental_taintObjectReference( 'A change handler cannot be passed to a client component', change, ); @@ -1525,7 +1525,7 @@ describe('ReactFlight', () => { SECRET: '3e971ecc1485fe78625598bf9b6f85db', }, }; - ReactServer.unstable_taintValue( + ReactServer.experimental_taintUniqueValue( 'Cannot pass a secret token to the client', process, process.env.SECRET, @@ -1556,7 +1556,7 @@ describe('ReactFlight', () => { name: 'Seb', token: BigInt('0x3e971ecc1485fe78625598bf9b6f85dc'), }; - ReactServer.unstable_taintValue( + ReactServer.experimental_taintUniqueValue( 'Cannot pass a secret token to the client', currentUser, currentUser.token, @@ -1587,7 +1587,7 @@ describe('ReactFlight', () => { name: 'Seb', token: new Uint32Array([0x3e971ecc, 0x1485fe78, 0x625598bf, 0x9b6f85dd]), }; - ReactServer.unstable_taintValue( + ReactServer.experimental_taintUniqueValue( 'Cannot pass a secret token to the client', currentUser, currentUser.token, @@ -1620,7 +1620,7 @@ describe('ReactFlight', () => { name: 'Seb', token: '3e971ecc1485fe78625598bf9b6f85db', }; - ReactServer.unstable_taintValue( + ReactServer.experimental_taintUniqueValue( 'Cannot pass a secret token to the client', user, user.token, diff --git a/packages/react/src/ReactSharedSubset.experimental.js b/packages/react/src/ReactSharedSubset.experimental.js index 94d142e1c22d5..45baaadc89f7b 100644 --- a/packages/react/src/ReactSharedSubset.experimental.js +++ b/packages/react/src/ReactSharedSubset.experimental.js @@ -16,8 +16,8 @@ export {default as __SECRET_SERVER_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} fr // These are server-only export { - taintValue as unstable_taintValue, - taintShallowObject as unstable_taintShallowObject, + taintUniqueValue as experimental_taintUniqueValue, + taintObjectReference as experimental_taintObjectReference, } from './ReactTaint'; export { diff --git a/packages/react/src/ReactTaint.js b/packages/react/src/ReactTaint.js index cc54f89b17688..51afbf60d4749 100644 --- a/packages/react/src/ReactTaint.js +++ b/packages/react/src/ReactTaint.js @@ -52,7 +52,7 @@ const finalizationRegistry = ? new FinalizationRegistry(cleanup) : null; -export function taintValue( +export function taintUniqueValue( message: ?string, lifetime: Reference, value: string | bigint | $ArrayBufferView, @@ -89,7 +89,7 @@ export function taintValue( const kind = value === null ? 'null' : typeof value; if (kind === 'object' || kind === 'function') { throw new Error( - 'taintValue cannot taint objects or functions. Try taintShallowObject instead.', + 'taintUniqueValue cannot taint objects or functions. Try taintObjectReference instead.', ); } throw new Error( @@ -113,7 +113,10 @@ export function taintValue( } } -export function taintShallowObject(message: ?string, object: Reference): void { +export function taintObjectReference( + message: ?string, + object: Reference, +): void { if (!enableTaint) { throw new Error('Not implemented.'); } @@ -121,7 +124,7 @@ export function taintShallowObject(message: ?string, object: Reference): void { message = '' + (message || defaultMessage); if (typeof object === 'string' || typeof object === 'bigint') { throw new Error( - 'Only objects or functions can be passed to taintShallowObject. Try taintValue instead.', + 'Only objects or functions can be passed to taintObjectReference. Try taintUniqueValue instead.', ); } if ( @@ -129,7 +132,7 @@ export function taintShallowObject(message: ?string, object: Reference): void { (typeof object !== 'object' && typeof object !== 'function') ) { throw new Error( - 'Only objects or functions can be passed to taintShallowObject.', + 'Only objects or functions can be passed to taintObjectReference.', ); } TaintRegistryObjects.set(object, message); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 373bc8762c68c..2b29b27be8b2e 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -479,8 +479,8 @@ "491": "It should not be possible to postpone both at the root of an element as well as a slot below. This is a bug in React.", "492": "The \"react\" package in this environment is not configured correctly. The \"react-server\" condition must be enabled in any environment that runs React Server Components.", "493": "To taint a value, a life time must be defined by passing an object that holds the value.", - "494": "taintValue cannot taint objects or functions. Try taintShallowObject instead.", + "494": "taintUniqueValue cannot taint objects or functions. Try taintObjectReference instead.", "495": "Cannot taint a %s because the value is too general and cannot be a secret by", - "496": "Only objects or functions can be passed to taintShallowObject. Try taintValue instead.", - "497": "Only objects or functions can be passed to taintShallowObject." + "496": "Only objects or functions can be passed to taintObjectReference. Try taintUniqueValue instead.", + "497": "Only objects or functions can be passed to taintObjectReference." } \ No newline at end of file From c2e8442fae9f14ea759a46cf9a7e498615be5f4e Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 2 Oct 2023 12:09:21 -0400 Subject: [PATCH 9/9] Typo --- packages/react/src/ReactTaint.js | 5 ++--- scripts/error-codes/codes.json | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/react/src/ReactTaint.js b/packages/react/src/ReactTaint.js index 51afbf60d4749..d9e532737be6e 100644 --- a/packages/react/src/ReactTaint.js +++ b/packages/react/src/ReactTaint.js @@ -67,7 +67,7 @@ export function taintUniqueValue( (typeof lifetime !== 'object' && typeof lifetime !== 'function') ) { throw new Error( - 'To taint a value, a life time must be defined by passing an object that holds ' + + 'To taint a value, a lifetime must be defined by passing an object that holds ' + 'the value.', ); } @@ -95,8 +95,7 @@ export function taintUniqueValue( throw new Error( 'Cannot taint a ' + kind + - ' because the value is too general and cannot be ' + - 'a secret by', + ' because the value is too general and not unique enough to block globally.', ); } const existingEntry = TaintRegistryValues.get(entryValue); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 2b29b27be8b2e..be62358481bc0 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -478,9 +478,9 @@ "490": "Expected to see a Suspense boundary in this slot. The tree doesn't match so React will fallback to client rendering.", "491": "It should not be possible to postpone both at the root of an element as well as a slot below. This is a bug in React.", "492": "The \"react\" package in this environment is not configured correctly. The \"react-server\" condition must be enabled in any environment that runs React Server Components.", - "493": "To taint a value, a life time must be defined by passing an object that holds the value.", + "493": "To taint a value, a lifetime must be defined by passing an object that holds the value.", "494": "taintUniqueValue cannot taint objects or functions. Try taintObjectReference instead.", - "495": "Cannot taint a %s because the value is too general and cannot be a secret by", + "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." } \ No newline at end of file