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',