[DevTools] Enable minimal support in pages with `sandbox` Content-Security-Policy (#35208)
Yukimasa Funaoka committed
Jan 14, 2026 at 01:49 UTC
583e20033220bc17a590a25624f6251c6b101ff4
11 files changed
+390
-54
packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js
+8
@@ -17,6 +17,14 @@ const contentScriptsToInject = [
17
runAt: 'document_end',
18
world: chrome.scripting.ExecutionWorld.ISOLATED,
19
},
20
+ {
21
+ id: '@react-devtools/fallback-eval-context',
22
+ js: ['build/fallbackEvalContext.js'],
23
+ matches: ['<all_urls>'],
24
+ persistAcrossSessions: true,
25
+ runAt: 'document_start',
26
+ world: chrome.scripting.ExecutionWorld.MAIN,
27
+ },
28
{
29
id: '@react-devtools/hook',
30
js: ['build/installHook.js'],
packages/react-devtools-extensions/src/background/messageHandlers.js
+52
@@ -97,6 +97,58 @@ export function handleDevToolsPageMessage(message) {
97
98
break;
99
}
100
+
101
+ case 'eval-in-inspected-window': {
102
+ const {
103
+ payload: {tabId, requestId, scriptId, args},
104
+ } = message;
105
+
106
+ chrome.tabs
107
+ .sendMessage(tabId, {
108
+ source: 'devtools-page-eval',
109
+ payload: {
110
+ scriptId,
111
+ args,
112
+ },
113
+ })
114
+ .then(response => {
115
+ if (!response) {
116
+ chrome.runtime.sendMessage({
117
+ source: 'react-devtools-background',
118
+ payload: {
119
+ type: 'eval-in-inspected-window-response',
120
+ requestId,
121
+ result: null,
122
+ error: 'No response from content script',
123
+ },
124
+ });
125
+ return;
126
+ }
127
+ const {result, error} = response;
128
+ chrome.runtime.sendMessage({
129
+ source: 'react-devtools-background',
130
+ payload: {
131
+ type: 'eval-in-inspected-window-response',
132
+ requestId,
133
+ result,
134
+ error,
135
+ },
136
+ });
137
+ })
138
+ .catch(error => {
139
+ chrome.runtime.sendMessage({
140
+ source: 'react-devtools-background',
141
+ payload: {
142
+ type: 'eval-in-inspected-window-response',
143
+ requestId,
144
+ result: null,
145
+ error: error?.message || String(error),
146
+ },
147
+ });
148
+ });
149
+
150
+ break;
151
+ }
152
}
153
}
154
packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js
new
+35
@@ -0,0 +1,35 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import {evalScripts} from '../evalScripts';
11
+
12
+window.addEventListener('message', event => {
13
+ if (event.data?.source === 'react-devtools-content-script-eval') {
14
+ const {scriptId, args, requestId} = event.data.payload;
15
+ const response = {result: null, error: null};
16
+ try {
17
+ if (!evalScripts[scriptId]) {
18
+ throw new Error(`No eval script with id "${scriptId}" exists.`);
19
+ }
20
+ response.result = evalScripts[scriptId].fn.apply(null, args);
21
+ } catch (err) {
22
+ response.error = err.message;
23
+ }
24
+ window.postMessage(
25
+ {
26
+ source: 'react-devtools-content-script-eval-response',
27
+ payload: {
28
+ requestId,
29
+ response,
30
+ },
31
+ },
32
+ '*',
33
+ );
34
+ }
35
+});
packages/react-devtools-extensions/src/contentScripts/proxy.js
+46
@@ -117,3 +117,49 @@ function connectPort() {
117
// $FlowFixMe[incompatible-use]
118
port.onDisconnect.addListener(handleDisconnect);
119
}
120
+
121
+let evalRequestId = 0;
122
+const evalRequestCallbacks = new Map<number, Function>();
123
+
124
+chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
125
+ switch (msg?.source) {
126
+ case 'devtools-page-eval': {
127
+ const {scriptId, args} = msg.payload;
128
+ const requestId = evalRequestId++;
129
+ window.postMessage(
130
+ {
131
+ source: 'react-devtools-content-script-eval',
132
+ payload: {
133
+ requestId,
134
+ scriptId,
135
+ args,
136
+ },
137
+ },
138
+ '*',
139
+ );
140
+ evalRequestCallbacks.set(requestId, sendResponse);
141
+ return true; // Indicate we will respond asynchronously
142
+ }
143
+ }
144
+});
145
+
146
+window.addEventListener('message', event => {
147
+ if (event.data?.source === 'react-devtools-content-script-eval-response') {
148
+ const {requestId, response} = event.data.payload;
149
+ const callback = evalRequestCallbacks.get(requestId);
150
+ try {
151
+ if (!callback)
152
+ throw new Error(
153
+ `No eval request callback for id "${requestId}" exists.`,
154
+ );
155
+ callback(response);
156
+ } catch (e) {
157
+ console.warn(
158
+ 'React DevTools Content Script eval response error occurred:',
159
+ e,
160
+ );
161
+ } finally {
162
+ evalRequestCallbacks.delete(requestId);
163
+ }
164
+ }
165
+});
packages/react-devtools-extensions/src/evalScripts.js
new
+112
@@ -0,0 +1,112 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+export type EvalScriptIds =
11
+ | 'checkIfReactPresentInInspectedWindow'
12
+ | 'reload'
13
+ | 'setBrowserSelectionFromReact'
14
+ | 'setReactSelectionFromBrowser'
15
+ | 'viewAttributeSource'
16
+ | 'viewElementSource';
17
+
18
+/*
19
+ .fn for fallback in Content Script context
20
+ .code for chrome.devtools.inspectedWindow.eval()
21
+*/
22
+type EvalScriptEntry = {
23
+ fn: (...args: any[]) => any,
24
+ code: (...args: any[]) => string,
25
+};
26
+
27
+/*
28
+ Can not access `Developer Tools Console API` (e.g., inspect(), $0) in this context.
29
+ So some fallback functions are no-op or throw error.
30
+*/
31
+export const evalScripts: {[key: EvalScriptIds]: EvalScriptEntry} = {
32
+ checkIfReactPresentInInspectedWindow: {
33
+ fn: () =>
34
+ window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&
35
+ window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0,
36
+ code: () =>
37
+ 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&' +
38
+ 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
39
+ },
40
+ reload: {
41
+ fn: () => window.location.reload(),
42
+ code: () => 'window.location.reload();',
43
+ },
44
+ setBrowserSelectionFromReact: {
45
+ fn: () => {
46
+ throw new Error('Not supported in fallback eval context');
47
+ },
48
+ code: () =>
49
+ '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
50
+ '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
51
+ 'false',
52
+ },
53
+ setReactSelectionFromBrowser: {
54
+ fn: () => {
55
+ throw new Error('Not supported in fallback eval context');
56
+ },
57
+ code: () =>
58
+ '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
59
+ '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
60
+ 'false',
61
+ },
62
+ viewAttributeSource: {
63
+ fn: ({rendererID, elementID, path}) => {
64
+ return false; // Not supported in fallback eval context
65
+ },
66
+ code: ({rendererID, elementID, path}) =>
67
+ '{' + // The outer block is important because it means we can declare local variables.
68
+ 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
69
+ JSON.stringify(rendererID) +
70
+ ');' +
71
+ 'if (renderer) {' +
72
+ ' const value = renderer.getElementAttributeByPath(' +
73
+ JSON.stringify(elementID) +
74
+ ',' +
75
+ JSON.stringify(path) +
76
+ ');' +
77
+ ' if (value) {' +
78
+ ' inspect(value);' +
79
+ ' true;' +
80
+ ' } else {' +
81
+ ' false;' +
82
+ ' }' +
83
+ '} else {' +
84
+ ' false;' +
85
+ '}' +
86
+ '}',
87
+ },
88
+ viewElementSource: {
89
+ fn: ({rendererID, elementID}) => {
90
+ return false; // Not supported in fallback eval context
91
+ },
92
+ code: ({rendererID, elementID}) =>
93
+ '{' + // The outer block is important because it means we can declare local variables.
94
+ 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
95
+ JSON.stringify(rendererID) +
96
+ ');' +
97
+ 'if (renderer) {' +
98
+ ' const value = renderer.getElementSourceFunctionById(' +
99
+ JSON.stringify(elementID) +
100
+ ');' +
101
+ ' if (value) {' +
102
+ ' inspect(value);' +
103
+ ' true;' +
104
+ ' } else {' +
105
+ ' false;' +
106
+ ' }' +
107
+ '} else {' +
108
+ ' false;' +
109
+ '}' +
110
+ '}',
111
+ },
112
+};
packages/react-devtools-extensions/src/main/elementSelection.js
+7
-9
@@ -1,13 +1,12 @@
1
-/* global chrome */
1
+import {evalInInspectedWindow} from './evalInInspectedWindow';
2
3
export function setBrowserSelectionFromReact() {
4
// This is currently only called on demand when you press "view DOM".
5
// In the future, if Chrome adds an inspect() that doesn't switch tabs,
6
// we could make this happen automatically when you select another component.
7
- chrome.devtools.inspectedWindow.eval(
8
- '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
9
- '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
10
- 'false',
7
+ evalInInspectedWindow(
8
+ 'setBrowserSelectionFromReact',
9
+ [],
10
(didSelectionChange, evalError) => {
11
if (evalError) {
12
console.error(evalError);
@@ -19,10 +18,9 @@ export function setBrowserSelectionFromReact() {
18
export function setReactSelectionFromBrowser(bridge) {
19
// When the user chooses a different node in the browser Elements tab,
20
// copy it over to the hook object so that we can sync the selection.
22
- chrome.devtools.inspectedWindow.eval(
23
- '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
24
- '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
25
- 'false',
21
+ evalInInspectedWindow(
22
+ 'setReactSelectionFromBrowser',
23
+ [],
24
(didSelectionChange, evalError) => {
25
if (evalError) {
26
console.error(evalError);
packages/react-devtools-extensions/src/main/evalInInspectedWindow.js
new
+116
@@ -0,0 +1,116 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {EvalScriptIds} from '../evalScripts';
11
+
12
+import {evalScripts} from '../evalScripts';
13
+
14
+type ExceptionInfo = {
15
+ code: ?string,
16
+ description: ?string,
17
+ isError: boolean,
18
+ isException: boolean,
19
+ value: any,
20
+};
21
+
22
+const EVAL_TIMEOUT = 1000 * 10;
23
+
24
+let evalRequestId = 0;
25
+const evalRequestCallbacks = new Map<
26
+ number,
27
+ (value: {result: any, error: any}) => void,
28
+>();
29
+
30
+function fallbackEvalInInspectedWindow(
31
+ scriptId: EvalScriptIds,
32
+ args: any[],
33
+ callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
34
+) {
35
+ if (!evalScripts[scriptId]) {
36
+ throw new Error(`No eval script with id "${scriptId}" exists.`);
37
+ }
38
+ const code = evalScripts[scriptId].code.apply(null, args);
39
+ const tabId = chrome.devtools.inspectedWindow.tabId;
40
+ const requestId = evalRequestId++;
41
+ chrome.runtime.sendMessage({
42
+ source: 'devtools-page',
43
+ payload: {
44
+ type: 'eval-in-inspected-window',
45
+ tabId,
46
+ requestId,
47
+ scriptId,
48
+ args,
49
+ },
50
+ });
51
+ const timeout = setTimeout(() => {
52
+ evalRequestCallbacks.delete(requestId);
53
+ if (callback) {
54
+ callback(null, {
55
+ code,
56
+ description:
57
+ 'Timed out while waiting for eval response from the inspected window.',
58
+ isError: true,
59
+ isException: false,
60
+ value: undefined,
61
+ });
62
+ }
63
+ }, EVAL_TIMEOUT);
64
+ evalRequestCallbacks.set(requestId, ({result, error}) => {
65
+ clearTimeout(timeout);
66
+ evalRequestCallbacks.delete(requestId);
67
+ if (callback) {
68
+ if (error) {
69
+ callback(null, {
70
+ code,
71
+ description: undefined,
72
+ isError: false,
73
+ isException: true,
74
+ value: error,
75
+ });
76
+ return;
77
+ }
78
+ callback(result, null);
79
+ }
80
+ });
81
+}
82
+
83
+export function evalInInspectedWindow(
84
+ scriptId: EvalScriptIds,
85
+ args: any[],
86
+ callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
87
+) {
88
+ if (!evalScripts[scriptId]) {
89
+ throw new Error(`No eval script with id "${scriptId}" exists.`);
90
+ }
91
+ const code = evalScripts[scriptId].code.apply(null, args);
92
+ chrome.devtools.inspectedWindow.eval(code, (result, exceptionInfo) => {
93
+ if (!exceptionInfo) {
94
+ callback(result, exceptionInfo);
95
+ return;
96
+ }
97
+ // If an exception (e.g. CSP Blocked) occurred,
98
+ // fallback to the content script eval context
99
+ fallbackEvalInInspectedWindow(scriptId, args, callback);
100
+ });
101
+}
102
+
103
+chrome.runtime.onMessage.addListener(({payload, source}) => {
104
+ if (source === 'react-devtools-background') {
105
+ switch (payload?.type) {
106
+ case 'eval-in-inspected-window-response': {
107
+ const {requestId, result, error} = payload;
108
+ const callback = evalRequestCallbacks.get(requestId);
109
+ if (callback) {
110
+ callback({result, error});
111
+ }
112
+ break;
113
+ }
114
+ }
115
+ }
116
+});
packages/react-devtools-extensions/src/main/index.js
+2
-1
@@ -32,6 +32,7 @@ import {
32
} from './elementSelection';
33
import {viewAttributeSource} from './sourceSelection';
34
35
+import {evalInInspectedWindow} from './evalInInspectedWindow';
36
import {startReactPolling} from './reactPolling';
37
import {cloneStyleTags} from './cloneStyleTags';
38
import fetchFileWithCaching from './fetchFileWithCaching';
@@ -70,7 +71,7 @@ function createBridge() {
71
72
bridge.addListener('reloadAppForProfiling', () => {
73
localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
73
- chrome.devtools.inspectedWindow.eval('window.location.reload();');
74
+ evalInInspectedWindow('reload', []);
75
});
76
77
bridge.addListener(
packages/react-devtools-extensions/src/main/reactPolling.js
+4
-3
@@ -1,4 +1,4 @@
1
-/* global chrome */
1
+import {evalInInspectedWindow} from './evalInInspectedWindow';
2
3
class CouldNotFindReactOnThePageError extends Error {
4
constructor() {
@@ -26,8 +26,9 @@ export function startReactPolling(
26
27
// This function will call onSuccess only if React was found and polling is not aborted, onError will be called for every other case
28
function checkIfReactPresentInInspectedWindow(onSuccess, onError) {
29
- chrome.devtools.inspectedWindow.eval(
30
- 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
29
+ evalInInspectedWindow(
30
+ 'checkIfReactPresentInInspectedWindow',
31
+ [],
32
(pageHasReact, exceptionInfo) => {
33
if (status === 'aborted') {
34
onError(
packages/react-devtools-extensions/src/main/sourceSelection.js
+7
-41
@@ -1,27 +1,9 @@
1
-/* global chrome */
1
+import {evalInInspectedWindow} from './evalInInspectedWindow';
2
3
export function viewAttributeSource(rendererID, elementID, path) {
4
- chrome.devtools.inspectedWindow.eval(
5
- '{' + // The outer block is important because it means we can declare local variables.
6
- 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
7
- JSON.stringify(rendererID) +
8
- ');' +
9
- 'if (renderer) {' +
10
- ' const value = renderer.getElementAttributeByPath(' +
11
- JSON.stringify(elementID) +
12
- ',' +
13
- JSON.stringify(path) +
14
- ');' +
15
- ' if (value) {' +
16
- ' inspect(value);' +
17
- ' true;' +
18
- ' } else {' +
19
- ' false;' +
20
- ' }' +
21
- '} else {' +
22
- ' false;' +
23
- '}' +
24
- '}',
4
+ evalInInspectedWindow(
5
+ 'viewAttributeSource',
6
+ [{rendererID, elementID, path}],
7
(didInspect, evalError) => {
8
if (evalError) {
9
console.error(evalError);
@@ -31,25 +13,9 @@ export function viewAttributeSource(rendererID, elementID, path) {
13
}
14
15
export function viewElementSource(rendererID, elementID) {
34
- chrome.devtools.inspectedWindow.eval(
35
- '{' + // The outer block is important because it means we can declare local variables.
36
- 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
37
- JSON.stringify(rendererID) +
38
- ');' +
39
- 'if (renderer) {' +
40
- ' const value = renderer.getElementSourceFunctionById(' +
41
- JSON.stringify(elementID) +
42
- ');' +
43
- ' if (value) {' +
44
- ' inspect(value);' +
45
- ' true;' +
46
- ' } else {' +
47
- ' false;' +
48
- ' }' +
49
- '} else {' +
50
- ' false;' +
51
- '}' +
52
- '}',
16
+ evalInInspectedWindow(
17
+ 'viewElementSource',
18
+ [{rendererID, elementID}],
19
(didInspect, evalError) => {
20
if (evalError) {
21
console.error(evalError);
packages/react-devtools-extensions/webpack.config.js
+1
@@ -69,6 +69,7 @@ module.exports = {
69
backend: './src/backend.js',
70
background: './src/background/index.js',
71
backendManager: './src/contentScripts/backendManager.js',
72
+ fallbackEvalContext: './src/contentScripts/fallbackEvalContext.js',
73
fileFetcher: './src/contentScripts/fileFetcher.js',
74
main: './src/main/index.js',
75
panel: './src/panel.js',