@samitouri / QOS-React-1 / commits / 9f8daa6cb5

[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;