[Breaking] Remove disableJavaScriptURLs (#28615)
## Overview This has landed, so we can remove the flag ## Changelog This change blocks using javascript URLs such as: ```html <a href="javascript:notfine">p0wned</a> ``` We previously announced dropping support for this via a warning: > A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead.
Ricky committed
Mar 26, 2024 at 23:45 UTC
9f8daa6cb5aae476cf54611874ea7522243c6ba6
8 files changed
+5
-202
packages/react-dom-bindings/src/shared/sanitizeURL.js
+5
-22
@@ -7,8 +7,6 @@
7
* @flow
8
*/
9
10
-import {disableJavaScriptURLs} from 'shared/ReactFeatureFlags';
11
-
10
// A javascript: URL can contain leading C0 control or \u0020 SPACE,
11
// and any newline or tab are filtered out as if they're not part of the URL.
12
// https://url.spec.whatwg.org/#url-parsing
@@ -22,29 +20,14 @@ import {disableJavaScriptURLs} from 'shared/ReactFeatureFlags';
20
const isJavaScriptProtocol =
21
/^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i;
22
25
-let didWarn = false;
26
-
23
function sanitizeURL<T>(url: T): T | string {
24
// We should never have symbols here because they get filtered out elsewhere.
25
// eslint-disable-next-line react-internal/safe-string-coercion
30
- const stringifiedURL = '' + (url: any);
31
- if (disableJavaScriptURLs) {
32
- if (isJavaScriptProtocol.test(stringifiedURL)) {
33
- // Return a different javascript: url that doesn't cause any side-effects and just
34
- // throws if ever visited.
35
- // eslint-disable-next-line no-script-url
36
- return "javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')";
37
- }
38
- } else if (__DEV__) {
39
- if (!didWarn && isJavaScriptProtocol.test(stringifiedURL)) {
40
- didWarn = true;
41
- console.error(
42
- 'A future version of React will block javascript: URLs as a security precaution. ' +
43
- 'Use event handlers instead if you can. If you need to generate unsafe HTML try ' +
44
- 'using dangerouslySetInnerHTML instead. React was passed %s.',
45
- JSON.stringify(stringifiedURL),
46
- );
47
- }
26
+ if (isJavaScriptProtocol.test('' + (url: any))) {
27
+ // Return a different javascript: url that doesn't cause any side-effects and just
28
+ // throws if ever visited.
29
+ // eslint-disable-next-line no-script-url
30
+ return "javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')";
31
}
32
return url;
33
}
packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js
-170
@@ -23,176 +23,6 @@ const EXPECTED_SAFE_URL =
23
"javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')";
24
25
describe('ReactDOMServerIntegration - Untrusted URLs', () => {
26
- // The `itRenders` helpers don't work with the gate pragma, so we have to do
27
- // this instead.
28
- if (gate(flags => flags.disableJavaScriptURLs)) {
29
- it("empty test so Jest doesn't complain", () => {});
30
- return;
31
- }
32
-
33
- function initModules() {
34
- jest.resetModules();
35
- React = require('react');
36
- ReactDOMClient = require('react-dom/client');
37
- ReactDOMServer = require('react-dom/server');
38
- act = require('internal-test-utils').act;
39
-
40
- // Make them available to the helpers.
41
- return {
42
- ReactDOMClient,
43
- ReactDOMServer,
44
- };
45
- }
46
-
47
- const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
48
-
49
- beforeEach(() => {
50
- resetModules();
51
- });
52
-
53
- itRenders('a http link with the word javascript in it', async render => {
54
- const e = await render(
55
- <a href="http://javascript:0/thisisfine">Click me</a>,
56
- );
57
- expect(e.tagName).toBe('A');
58
- expect(e.href).toBe('http://javascript:0/thisisfine');
59
- });
60
-
61
- itRenders('a javascript protocol href', async render => {
62
- // Only the first one warns. The second warning is deduped.
63
- const e = await render(
64
- <div>
65
- <a href="javascript:notfine">p0wned</a>
66
- <a href="javascript:notfineagain">p0wned again</a>
67
- </div>,
68
- 1,
69
- );
70
- expect(e.firstChild.href).toBe('javascript:notfine');
71
- expect(e.lastChild.href).toBe('javascript:notfineagain');
72
- });
73
-
74
- itRenders('a javascript protocol with leading spaces', async render => {
75
- const e = await render(
76
- <a href={' \t \u0000\u001F\u0003javascript\n: notfine'}>p0wned</a>,
77
- 1,
78
- );
79
- // We use an approximate comparison here because JSDOM might not parse
80
- // \u0000 in HTML properly.
81
- expect(e.href).toContain('notfine');
82
- });
83
-
84
- itRenders(
85
- 'a javascript protocol with intermediate new lines and mixed casing',
86
- async render => {
87
- const e = await render(
88
- <a href={'\t\r\n Jav\rasCr\r\niP\t\n\rt\n:notfine'}>p0wned</a>,
89
- 1,
90
- );
91
- expect(e.href).toBe('javascript:notfine');
92
- },
93
- );
94
-
95
- itRenders('a javascript protocol area href', async render => {
96
- const e = await render(
97
- <map>
98
- <area href="javascript:notfine" />
99
- </map>,
100
- 1,
101
- );
102
- expect(e.firstChild.href).toBe('javascript:notfine');
103
- });
104
-
105
- itRenders('a javascript protocol form action', async render => {
106
- const e = await render(<form action="javascript:notfine">p0wned</form>, 1);
107
- expect(e.action).toBe('javascript:notfine');
108
- });
109
-
110
- itRenders('a javascript protocol input formAction', async render => {
111
- const e = await render(
112
- <input type="submit" formAction="javascript:notfine" />,
113
- 1,
114
- );
115
- expect(e.getAttribute('formAction')).toBe('javascript:notfine');
116
- });
117
-
118
- itRenders('a javascript protocol button formAction', async render => {
119
- const e = await render(
120
- <button formAction="javascript:notfine">p0wned</button>,
121
- 1,
122
- );
123
- expect(e.getAttribute('formAction')).toBe('javascript:notfine');
124
- });
125
-
126
- itRenders('a javascript protocol iframe src', async render => {
127
- const e = await render(<iframe src="javascript:notfine" />, 1);
128
- expect(e.src).toBe('javascript:notfine');
129
- });
130
-
131
- itRenders('a javascript protocol frame src', async render => {
132
- const e = await render(
133
- <html>
134
- <head />
135
- <frameset>
136
- <frame src="javascript:notfine" />
137
- </frameset>
138
- </html>,
139
- 1,
140
- );
141
- expect(e.lastChild.firstChild.src).toBe('javascript:notfine');
142
- });
143
-
144
- itRenders('a javascript protocol in an SVG link', async render => {
145
- const e = await render(
146
- <svg>
147
- <a href="javascript:notfine" />
148
- </svg>,
149
- 1,
150
- );
151
- expect(e.firstChild.getAttribute('href')).toBe('javascript:notfine');
152
- });
153
-
154
- itRenders(
155
- 'a javascript protocol in an SVG link with a namespace',
156
- async render => {
157
- const e = await render(
158
- <svg>
159
- <a xlinkHref="javascript:notfine" />
160
- </svg>,
161
- 1,
162
- );
163
- expect(
164
- e.firstChild.getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
165
- ).toBe('javascript:notfine');
166
- },
167
- );
168
-
169
- it('rejects a javascript protocol href if it is added during an update', async () => {
170
- const container = document.createElement('div');
171
- const root = ReactDOMClient.createRoot(container);
172
- await act(async () => {
173
- root.render(<a href="thisisfine">click me</a>);
174
- });
175
- await expect(async () => {
176
- await act(() => {
177
- root.render(<a href="javascript:notfine">click me</a>);
178
- });
179
- }).toErrorDev(
180
- 'Warning: A future version of React will block javascript: URLs as a security precaution. ' +
181
- 'Use event handlers instead if you can. If you need to generate unsafe HTML try using ' +
182
- 'dangerouslySetInnerHTML instead. React was passed "javascript:notfine".\n' +
183
- ' in a (at **)',
184
- );
185
- });
186
-});
187
-
188
-describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', () => {
189
- // The `itRenders` helpers don't work with the gate pragma, so we have to do
190
- // this instead.
191
- if (gate(flags => !flags.disableJavaScriptURLs)) {
192
- it("empty test so Jest doesn't complain", () => {});
193
- return;
194
- }
195
-
26
function initModules() {
27
jest.resetModules();
28
packages/shared/ReactFeatureFlags.js
-4
@@ -136,10 +136,6 @@ const __NEXT_MAJOR__ = __EXPERIMENTAL__;
136
// Removes legacy style context
137
export const disableLegacyContext = __NEXT_MAJOR__;
138
139
-// Not ready to break experimental yet.
140
-// Disable javascript: URL strings in href for XSS protection.
141
-export const disableJavaScriptURLs = __NEXT_MAJOR__;
142
-
139
// Not ready to break experimental yet.
140
// Modern <StrictMode /> behaviour aligns more with what components
141
// components will encounter in production, especially when used With <Offscreen />.
packages/shared/forks/ReactFeatureFlags.native-fb.js
-1
@@ -50,7 +50,6 @@ export const enableBinaryFlight = true;
50
export const enableTaint = true;
51
export const enablePostpone = false;
52
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
53
-export const disableJavaScriptURLs = true;
53
export const disableCommentsAsDOMContainers = true;
54
export const disableInputAttributeSyncing = false;
55
export const disableIEWorkarounds = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
-1
@@ -25,7 +25,6 @@ export const enableFetchInstrumentation = true;
25
export const enableBinaryFlight = true;
26
export const enableTaint = true;
27
export const enablePostpone = false;
28
-export const disableJavaScriptURLs = true;
28
export const disableCommentsAsDOMContainers = true;
29
export const disableInputAttributeSyncing = false;
30
export const disableIEWorkarounds = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
-1
@@ -25,7 +25,6 @@ export const enableFetchInstrumentation = false;
25
export const enableBinaryFlight = true;
26
export const enableTaint = true;
27
export const enablePostpone = false;
28
-export const disableJavaScriptURLs = true;
28
export const disableCommentsAsDOMContainers = true;
29
export const disableInputAttributeSyncing = false;
30
export const disableIEWorkarounds = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
-1
@@ -25,7 +25,6 @@ export const enableFetchInstrumentation = false;
25
export const enableBinaryFlight = true;
26
export const enableTaint = true;
27
export const enablePostpone = false;
28
-export const disableJavaScriptURLs = true;
28
export const disableCommentsAsDOMContainers = true;
29
export const disableInputAttributeSyncing = false;
30
export const disableIEWorkarounds = true;
packages/shared/forks/ReactFeatureFlags.www.js
-2
@@ -79,8 +79,6 @@ export const enableTaint = false;
79
80
export const enablePostpone = false;
81
82
-export const disableJavaScriptURLs = true;
83
-
82
// TODO: www currently relies on this feature. It's disabled in open source.
83
// Need to remove it.
84
export const disableCommentsAsDOMContainers = false;