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-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 30cfda19af6ec..c0f3d2bbb0b04 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; @@ -1446,4 +1463,202 @@ 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.experimental_taintObjectReference( + "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.experimental_taintObjectReference( + '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.experimental_taintUniqueValue( + '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.experimental_taintUniqueValue( + '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.experimental_taintUniqueValue( + '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']); + }); + + // @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.experimental_taintUniqueValue( + '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 230994011fb93..c4b26520b6016 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 = @@ -192,14 +198,42 @@ 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, }; -const ReactCurrentDispatcher = - ReactServerSharedInternals.ReactCurrentDispatcher; -const ReactCurrentCache = ReactServerSharedInternals.ReactCurrentCache; +const { + TaintRegistryObjects, + TaintRegistryValues, + TaintRegistryByteLengths, + TaintRegistryPendingRequests, + ReactCurrentDispatcher, + ReactCurrentCache, +} = ReactServerSharedInternals; + +function throwTaintViolation(message: string) { + // eslint-disable-next-line react-internal/prod-error-codes + 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); @@ -235,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, @@ -258,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] @@ -781,6 +820,18 @@ function serializeTypedArray( tag: string, typedArray: $ArrayBufferView, ): string { + if (enableTaint) { + 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; const bufferId = request.nextChunkId++; // TODO: Convert to little endian if that's not the server default. @@ -959,6 +1010,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 +1148,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 +1185,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 +1240,12 @@ function resolveModelToJSON( } if (typeof value === 'bigint') { + if (enableTaint) { + const tainted = TaintRegistryValues.get(value); + if (tainted !== undefined) { + throwTaintViolation(tainted.message); + } + } return serializeBigInt(value); } @@ -1198,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; @@ -1522,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 3e9b81f4ec149..7b0cef8cb0a01 100644 --- a/packages/react/src/ReactServerSharedInternals.js +++ b/packages/react/src/ReactServerSharedInternals.js @@ -7,10 +7,27 @@ import ReactCurrentDispatcher from './ReactCurrentDispatcher'; import ReactCurrentCache from './ReactCurrentCache'; +import { + TaintRegistryObjects, + TaintRegistryValues, + TaintRegistryByteLengths, + TaintRegistryPendingRequests, +} from './ReactTaintRegistry'; + +import {enableTaint} from 'shared/ReactFeatureFlags'; const ReactServerSharedInternals = { ReactCurrentDispatcher, ReactCurrentCache, }; +if (enableTaint) { + ReactServerSharedInternals.TaintRegistryObjects = TaintRegistryObjects; + ReactServerSharedInternals.TaintRegistryValues = TaintRegistryValues; + ReactServerSharedInternals.TaintRegistryByteLengths = + TaintRegistryByteLengths; + ReactServerSharedInternals.TaintRegistryPendingRequests = + TaintRegistryPendingRequests; +} + export default ReactServerSharedInternals; diff --git a/packages/react/src/ReactSharedSubset.experimental.js b/packages/react/src/ReactSharedSubset.experimental.js index 80d50805c23b4..45baaadc89f7b 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 { + taintUniqueValue as experimental_taintUniqueValue, + taintObjectReference as experimental_taintObjectReference, +} 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..d9e532737be6e --- /dev/null +++ b/packages/react/src/ReactTaint.js @@ -0,0 +1,138 @@ +/** + * 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, enableBinaryFlight} from 'shared/ReactFeatureFlags'; + +import binaryToComparableString from 'shared/binaryToComparableString'; + +import ReactServerSharedInternals from './ReactServerSharedInternals'; +const { + TaintRegistryObjects, + TaintRegistryValues, + TaintRegistryByteLengths, + TaintRegistryPendingRequests, +} = ReactServerSharedInternals; + +interface Reference {} + +// 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) { + TaintRegistryPendingRequests.forEach(function (requestQueue) { + requestQueue.push(entryValue); + entry.count++; + }); + 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 taintUniqueValue( + 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 lifetime must be defined by passing an object that holds ' + + 'the value.', + ); + } + let entryValue: string | bigint; + if (typeof value === 'string' || typeof value === 'bigint') { + // Use as is. + entryValue = value; + } else if ( + 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 + // 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; + if (kind === 'object' || kind === 'function') { + throw new Error( + 'taintUniqueValue cannot taint objects or functions. Try taintObjectReference instead.', + ); + } + throw new Error( + 'Cannot taint a ' + + kind + + ' because the value is too general and not unique enough to block globally.', + ); + } + 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 taintObjectReference( + 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 taintObjectReference. Try taintUniqueValue instead.', + ); + } + if ( + object === null || + (typeof object !== 'object' && typeof object !== 'function') + ) { + throw new Error( + 'Only objects or functions can be passed to taintObjectReference.', + ); + } + TaintRegistryObjects.set(object, message); +} diff --git a/packages/react/src/ReactTaintRegistry.js b/packages/react/src/ReactTaintRegistry.js new file mode 100644 index 0000000000000..d600e640b523c --- /dev/null +++ b/packages/react/src/ReactTaintRegistry.js @@ -0,0 +1,27 @@ +/** + * 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 + */ + +interface Reference {} + +type TaintEntry = { + message: string, + count: number, +}; + +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(); + +// 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(); 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/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/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; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 43d448ca977b4..be62358481bc0 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 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 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 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',