@samitouri / QOS-React / commits / 103378b1ea

Warn for javascript: URLs in DOM sinks (#15047)

* Prevent javascript protocol URLs * Just warn when disableJavaScriptURLs is false This avoids a breaking change. * Allow framesets * Allow <html> to be used in integration tests Full document renders requires server rendering so the client path just uses the hydration path in this case to simplify writing these tests. * Detect leading and intermediate characters and test mixed case These are considered valid javascript urls by browser so they must be included in the filter. This is an exact match according to the spec but maybe we should include a super set to be safer? * Test updates to ensure we have coverage there too * Fix toString invocation and Flow types Right now we invoke toString twice when we hydrate (three times with the flag off). Ideally we should only do it once even in this case but the code structure doesn't really allow for that right now. * s/itRejects/itRejectsRendering * Dedupe warning and add the unsafe URL to the warning message * Add test that fails if g is added to the sanitizer This only affects the prod version since the warning is deduped anyway. * Fix prod test

Sebastian Markbåge committed Mar 11, 2019 at 16:39 UTC 103378b1eada44561821b1c22ff54e0537cf9764
14 files changed +451 -22
packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.internal.js new
+271
@@ -0,0 +1,271 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 + * @emails react-core
8 + */
9 +
10 +/* eslint-disable no-script-url */
11 +
12 +'use strict';
13 +
14 +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
15 +
16 +let React;
17 +let ReactDOM;
18 +let ReactDOMServer;
19 +
20 +function runTests(itRenders, itRejectsRendering, expectToReject) {
21 + itRenders('a http link with the word javascript in it', async render => {
22 + const e = await render(
23 + <a href="http://javascript:0/thisisfine">Click me</a>,
24 + );
25 + expect(e.tagName).toBe('A');
26 + expect(e.href).toBe('http://javascript:0/thisisfine');
27 + });
28 +
29 + itRejectsRendering('a javascript protocol href', async render => {
30 + // Only the first one warns. The second warning is deduped.
31 + const e = await render(
32 + <div>
33 + <a href="javascript:notfine">p0wned</a>
34 + <a href="javascript:notfineagain">p0wned again</a>
35 + </div>,
36 + 1,
37 + );
38 + expect(e.firstChild.href).toBe('javascript:notfine');
39 + expect(e.lastChild.href).toBe('javascript:notfineagain');
40 + });
41 +
42 + itRejectsRendering(
43 + 'a javascript protocol with leading spaces',
44 + async render => {
45 + const e = await render(
46 + <a href={' \t \u0000\u001F\u0003javascript\n: notfine'}>p0wned</a>,
47 + 1,
48 + );
49 + // We use an approximate comparison here because JSDOM might not parse
50 + // \u0000 in HTML properly.
51 + expect(e.href).toContain('notfine');
52 + },
53 + );
54 +
55 + itRejectsRendering(
56 + 'a javascript protocol with intermediate new lines and mixed casing',
57 + async render => {
58 + const e = await render(
59 + <a href={'\t\r\n Jav\rasCr\r\niP\t\n\rt\n:notfine'}>p0wned</a>,
60 + 1,
61 + );
62 + expect(e.href).toBe('javascript:notfine');
63 + },
64 + );
65 +
66 + itRejectsRendering('a javascript protocol area href', async render => {
67 + const e = await render(
68 + <map>
69 + <area href="javascript:notfine" />
70 + </map>,
71 + 1,
72 + );
73 + expect(e.firstChild.href).toBe('javascript:notfine');
74 + });
75 +
76 + itRejectsRendering('a javascript protocol form action', async render => {
77 + const e = await render(<form action="javascript:notfine">p0wned</form>, 1);
78 + expect(e.action).toBe('javascript:notfine');
79 + });
80 +
81 + itRejectsRendering(
82 + 'a javascript protocol button formAction',
83 + async render => {
84 + const e = await render(<input formAction="javascript:notfine" />, 1);
85 + expect(e.getAttribute('formAction')).toBe('javascript:notfine');
86 + },
87 + );
88 +
89 + itRejectsRendering('a javascript protocol input formAction', async render => {
90 + const e = await render(
91 + <button formAction="javascript:notfine">p0wned</button>,
92 + 1,
93 + );
94 + expect(e.getAttribute('formAction')).toBe('javascript:notfine');
95 + });
96 +
97 + itRejectsRendering('a javascript protocol iframe src', async render => {
98 + const e = await render(<iframe src="javascript:notfine" />, 1);
99 + expect(e.src).toBe('javascript:notfine');
100 + });
101 +
102 + itRejectsRendering('a javascript protocol frame src', async render => {
103 + const e = await render(
104 + <html>
105 + <head />
106 + <frameset>
107 + <frame src="javascript:notfine" />
108 + </frameset>
109 + </html>,
110 + 1,
111 + );
112 + expect(e.lastChild.firstChild.src).toBe('javascript:notfine');
113 + });
114 +
115 + itRejectsRendering('a javascript protocol in an SVG link', async render => {
116 + const e = await render(
117 + <svg>
118 + <a href="javascript:notfine" />
119 + </svg>,
120 + 1,
121 + );
122 + expect(e.firstChild.getAttribute('href')).toBe('javascript:notfine');
123 + });
124 +
125 + itRejectsRendering(
126 + 'a javascript protocol in an SVG link with a namespace',
127 + async render => {
128 + const e = await render(
129 + <svg>
130 + <a xlinkHref="javascript:notfine" />
131 + </svg>,
132 + 1,
133 + );
134 + expect(
135 + e.firstChild.getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
136 + ).toBe('javascript:notfine');
137 + },
138 + );
139 +
140 + it('rejects a javascript protocol href if it is added during an update', () => {
141 + let container = document.createElement('div');
142 + ReactDOM.render(<a href="thisisfine">click me</a>, container);
143 + expectToReject(() => {
144 + ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
145 + });
146 + });
147 +}
148 +
149 +describe('ReactDOMServerIntegration - Untrusted URLs', () => {
150 + function initModules() {
151 + jest.resetModuleRegistry();
152 + React = require('react');
153 + ReactDOM = require('react-dom');
154 + ReactDOMServer = require('react-dom/server');
155 +
156 + // Make them available to the helpers.
157 + return {
158 + ReactDOM,
159 + ReactDOMServer,
160 + };
161 + }
162 +
163 + const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
164 +
165 + beforeEach(() => {
166 + resetModules();
167 + });
168 +
169 + runTests(itRenders, itRenders, fn =>
170 + expect(fn).toWarnDev(
171 + 'Warning: A future version of React will block javascript: URLs as a security precaution. ' +
172 + 'Use event handlers instead if you can. If you need to generate unsafe HTML try using ' +
173 + 'dangerouslySetInnerHTML instead. React was passed "javascript:notfine".\n' +
174 + ' in a (at **)',
175 + ),
176 + );
177 +});
178 +
179 +describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', () => {
180 + function initModules() {
181 + jest.resetModuleRegistry();
182 + const ReactFeatureFlags = require('shared/ReactFeatureFlags');
183 + ReactFeatureFlags.disableJavaScriptURLs = true;
184 +
185 + React = require('react');
186 + ReactDOM = require('react-dom');
187 + ReactDOMServer = require('react-dom/server');
188 +
189 + // Make them available to the helpers.
190 + return {
191 + ReactDOM,
192 + ReactDOMServer,
193 + };
194 + }
195 +
196 + const {
197 + resetModules,
198 + itRenders,
199 + itThrowsWhenRendering,
200 + clientRenderOnBadMarkup,
201 + clientRenderOnServerString,
202 + } = ReactDOMServerIntegrationUtils(initModules);
203 +
204 + const expectToReject = fn => {
205 + let msg;
206 + try {
207 + fn();
208 + } catch (x) {
209 + msg = x.message;
210 + }
211 + expect(msg).toContain(
212 + 'React has blocked a javascript: URL as a security precaution.',
213 + );
214 + };
215 +
216 + beforeEach(() => {
217 + resetModules();
218 + });
219 +
220 + runTests(
221 + itRenders,
222 + (message, test) =>
223 + itThrowsWhenRendering(message, test, 'blocked a javascript: URL'),
224 + expectToReject,
225 + );
226 +
227 + itRenders('only the first invocation of toString', async render => {
228 + let expectedToStringCalls = 1;
229 + if (render === clientRenderOnBadMarkup) {
230 + // It gets called once on the server and once on the client
231 + // which happens to share the same object in our test runner.
232 + expectedToStringCalls = 2;
233 + }
234 + if (render === clientRenderOnServerString && __DEV__) {
235 + // The hydration validation calls it one extra time.
236 + // TODO: It would be good if we only called toString once for
237 + // consistency but the code structure makes that hard right now.
238 + expectedToStringCalls = 2;
239 + }
240 +
241 + let toStringCalls = 0;
242 + let firstIsSafe = {
243 + toString() {
244 + // This tries to avoid the validation by pretending to be safe
245 + // the first times it is called and then becomes dangerous.
246 + toStringCalls++;
247 + if (toStringCalls <= expectedToStringCalls) {
248 + return 'https://fb.me/';
249 + }
250 + return 'javascript:notfine';
251 + },
252 + };
253 +
254 + const e = await render(<a href={firstIsSafe} />);
255 + expect(toStringCalls).toBe(expectedToStringCalls);
256 + expect(e.href).toBe('https://fb.me/');
257 + });
258 +
259 + it('rejects a javascript protocol href if it is added during an update twice', () => {
260 + let container = document.createElement('div');
261 + ReactDOM.render(<a href="thisisfine">click me</a>, container);
262 + expectToReject(() => {
263 + ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
264 + });
265 + // The second update ensures that a global flag hasn't been added to the regex
266 + // which would fail to match the second time it is called.
267 + expectToReject(() => {
268 + ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
269 + });
270 + });
271 +});
packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js
+58 -20
@@ -19,6 +19,28 @@ module.exports = function(initModules) {
19 ({ReactDOM, ReactDOMServer} = initModules());
20 }
21
22 + function shouldUseDocument(reactElement) {
23 + // Used for whole document tests.
24 + return reactElement && reactElement.type === 'html';
25 + }
26 +
27 + function getContainerFromMarkup(reactElement, markup) {
28 + if (shouldUseDocument(reactElement)) {
29 + const doc = document.implementation.createHTMLDocument('');
30 + doc.open();
31 + doc.write(
32 + markup ||
33 + '<!doctype html><html><meta charset=utf-8><title>test doc</title>',
34 + );
35 + doc.close();
36 + return doc;
37 + } else {
38 + const container = document.createElement('div');
39 + container.innerHTML = markup;
40 + return container;
41 + }
42 + }
43 +
44 // Helper functions for rendering tests
45 // ====================================
46
@@ -97,9 +119,7 @@ module.exports = function(initModules) {
119 // Does not render on client or perform client-side revival.
120 async function serverRender(reactElement, errorCount = 0) {
121 const markup = await renderIntoString(reactElement, errorCount);
100 - const domElement = document.createElement('div');
101 - domElement.innerHTML = markup;
102 - return domElement.firstChild;
122 + return getContainerFromMarkup(reactElement, markup).firstChild;
123 }
124
125 // this just drains a readable piped into it to a string, which can be accessed
@@ -133,27 +153,28 @@ module.exports = function(initModules) {
153 // Does not render on client or perform client-side revival.
154 async function streamRender(reactElement, errorCount = 0) {
155 const markup = await renderIntoStream(reactElement, errorCount);
136 - const domElement = document.createElement('div');
137 - domElement.innerHTML = markup;
138 - return domElement.firstChild;
156 + return getContainerFromMarkup(reactElement, markup).firstChild;
157 }
158
159 const clientCleanRender = (element, errorCount = 0) => {
142 - const div = document.createElement('div');
143 - return renderIntoDom(element, div, false, errorCount);
160 + if (shouldUseDocument(element)) {
161 + // Documents can't be rendered from scratch.
162 + return clientRenderOnServerString(element, errorCount);
163 + }
164 + const container = document.createElement('div');
165 + return renderIntoDom(element, container, false, errorCount);
166 };
167
168 const clientRenderOnServerString = async (element, errorCount = 0) => {
169 const markup = await renderIntoString(element, errorCount);
170 resetModules();
171
150 - const domElement = document.createElement('div');
151 - domElement.innerHTML = markup;
152 - let serverNode = domElement.firstChild;
172 + let container = getContainerFromMarkup(element, markup);
173 + let serverNode = container.firstChild;
174
175 const firstClientNode = await renderIntoDom(
176 element,
156 - domElement,
177 + container,
178 true,
179 errorCount,
180 );
@@ -178,19 +199,35 @@ module.exports = function(initModules) {
199
200 const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
201 // First we render the top of bad mark up.
181 - const domElement = document.createElement('div');
182 - domElement.innerHTML =
183 - '<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
184 - await renderIntoDom(element, domElement, true, errorCount + 1);
202 +
203 + let container = getContainerFromMarkup(
204 + element,
205 + shouldUseDocument(element)
206 + ? '<html><body><div id="badIdWhichWillCauseMismatch" /></body></html>'
207 + : '<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>',
208 + );
209 +
210 + await renderIntoDom(element, container, true, errorCount + 1);
211
212 // This gives us the resulting text content.
187 - const hydratedTextContent = domElement.textContent;
213 + const hydratedTextContent =
214 + container.lastChild && container.lastChild.textContent;
215
216 // Next we render the element into a clean DOM node client side.
190 - const cleanDomElement = document.createElement('div');
191 - await asyncReactDOMRender(element, cleanDomElement, true);
217 + let cleanContainer;
218 + if (shouldUseDocument(element)) {
219 + // We can't render into a document during a clean render,
220 + // so instead, we'll render the children into the document element.
221 + cleanContainer = getContainerFromMarkup(element, '<html></html>')
222 + .documentElement;
223 + element = element.props.children;
224 + } else {
225 + cleanContainer = document.createElement('div');
226 + }
227 + await asyncReactDOMRender(element, cleanContainer, true);
228 // This gives us the expected text content.
193 - const cleanTextContent = cleanDomElement.textContent;
229 + const cleanTextContent =
230 + cleanContainer.lastChild && cleanContainer.lastChild.textContent;
231
232 // The only guarantee is that text content has been patched up if needed.
233 expect(hydratedTextContent).toBe(cleanTextContent);
@@ -320,6 +357,7 @@ module.exports = function(initModules) {
357 asyncReactDOMRender,
358 serverRender,
359 clientCleanRender,
360 + clientRenderOnBadMarkup,
361 clientRenderOnServerString,
362 renderIntoDom,
363 streamRender,
packages/react-dom/src/client/DOMPropertyOperations.js
+12
@@ -15,6 +15,8 @@ import {
15 BOOLEAN,
16 OVERLOADED_BOOLEAN,
17 } from '../shared/DOMProperty';
18 +import sanitizeURL from '../shared/sanitizeURL';
19 +import {disableJavaScriptURLs} from 'shared/ReactFeatureFlags';
20
21 import type {PropertyInfo} from '../shared/DOMProperty';
22
@@ -34,6 +36,13 @@ export function getValueForProperty(
36 const {propertyName} = propertyInfo;
37 return (node: any)[propertyName];
38 } else {
39 + if (!disableJavaScriptURLs && propertyInfo.sanitizeURL) {
40 + // If we haven't fully disabled javascript: URLs, and if
41 + // the hydration is successful of a javascript: URL, we
42 + // still want to warn on the client.
43 + sanitizeURL('' + (expected: any));
44 + }
45 +
46 const attributeName = propertyInfo.attributeName;
47
48 let stringValue = null;
@@ -164,6 +173,9 @@ export function setValueForProperty(
173 // `setAttribute` with objects becomes only `[object]` in IE8/9,
174 // ('' + value) makes it output the correct toString()-value.
175 attributeValue = '' + (value: any);
176 + if (propertyInfo.sanitizeURL) {
177 + sanitizeURL(attributeValue);
178 + }
179 }
180 if (attributeNamespace) {
181 node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
packages/react-dom/src/client/validateDOMNesting.js
+4 -1
@@ -282,7 +282,9 @@ if (__DEV__) {
282 );
283 // https://html.spec.whatwg.org/multipage/semantics.html#the-html-element
284 case 'html':
285 - return tag === 'head' || tag === 'body';
285 + return tag === 'head' || tag === 'body' || tag === 'frameset';
286 + case 'frameset':
287 + return tag === 'frame';
288 case '#document':
289 return tag === 'html';
290 }
@@ -314,6 +316,7 @@ if (__DEV__) {
316 case 'caption':
317 case 'col':
318 case 'colgroup':
319 + case 'frameset':
320 case 'frame':
321 case 'head':
322 case 'html':
packages/react-dom/src/server/DOMMarkupOperations.js
+5
@@ -17,6 +17,7 @@ import {
17 shouldIgnoreAttribute,
18 shouldRemoveAttribute,
19 } from '../shared/DOMProperty';
20 +import sanitizeURL from '../shared/sanitizeURL';
21 import quoteAttributeValueForBrowser from './quoteAttributeValueForBrowser';
22
23 /**
@@ -58,6 +59,10 @@ export function createMarkupForProperty(name: string, value: mixed): string {
59 if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) {
60 return attributeName + '=""';
61 } else {
62 + if (propertyInfo.sanitizeURL) {
63 + value = '' + (value: any);
64 + sanitizeURL(value);
65 + }
66 return attributeName + '=' + quoteAttributeValueForBrowser(value);
67 }
68 } else if (isAttributeNameSafe(name)) {
packages/react-dom/src/shared/DOMProperty.js
+39 -1
@@ -51,6 +51,7 @@ export type PropertyInfo = {|
51 +mustUseProperty: boolean,
52 +propertyName: string,
53 +type: PropertyType,
54 + +sanitizeURL: boolean,
55 |};
56
57 /* eslint-disable max-len */
@@ -186,6 +187,7 @@ function PropertyInfoRecord(
187 mustUseProperty: boolean,
188 attributeName: string,
189 attributeNamespace: string | null,
190 + sanitizeURL: boolean,
191 ) {
192 this.acceptsBooleans =
193 type === BOOLEANISH_STRING ||
@@ -196,6 +198,7 @@ function PropertyInfoRecord(
198 this.mustUseProperty = mustUseProperty;
199 this.propertyName = name;
200 this.type = type;
201 + this.sanitizeURL = sanitizeURL;
202 }
203
204 // When adding attributes to this list, be sure to also add them to
@@ -223,6 +226,7 @@ const properties = {};
226 false, // mustUseProperty
227 name, // attributeName
228 null, // attributeNamespace
229 + false, // sanitizeURL
230 );
231 });
232
@@ -240,6 +244,7 @@ const properties = {};
244 false, // mustUseProperty
245 attributeName, // attributeName
246 null, // attributeNamespace
247 + false, // sanitizeURL
248 );
249 });
250
@@ -253,6 +258,7 @@ const properties = {};
258 false, // mustUseProperty
259 name.toLowerCase(), // attributeName
260 null, // attributeNamespace
261 + false, // sanitizeURL
262 );
263 });
264
@@ -272,6 +278,7 @@ const properties = {};
278 false, // mustUseProperty
279 name, // attributeName
280 null, // attributeNamespace
281 + false, // sanitizeURL
282 );
283 });
284
@@ -308,6 +315,7 @@ const properties = {};
315 false, // mustUseProperty
316 name.toLowerCase(), // attributeName
317 null, // attributeNamespace
318 + false, // sanitizeURL
319 );
320 });
321
@@ -331,6 +339,7 @@ const properties = {};
339 true, // mustUseProperty
340 name, // attributeName
341 null, // attributeNamespace
342 + false, // sanitizeURL
343 );
344 });
345
@@ -350,6 +359,7 @@ const properties = {};
359 false, // mustUseProperty
360 name, // attributeName
361 null, // attributeNamespace
362 + false, // sanitizeURL
363 );
364 });
365
@@ -370,6 +380,7 @@ const properties = {};
380 false, // mustUseProperty
381 name, // attributeName
382 null, // attributeNamespace
383 + false, // sanitizeURL
384 );
385 });
386
@@ -381,6 +392,7 @@ const properties = {};
392 false, // mustUseProperty
393 name.toLowerCase(), // attributeName
394 null, // attributeNamespace
395 + false, // sanitizeURL
396 );
397 });
398
@@ -478,6 +490,7 @@ const capitalize = token => token[1].toUpperCase();
490 false, // mustUseProperty
491 attributeName,
492 null, // attributeNamespace
493 + false, // sanitizeURL
494 );
495 });
496
@@ -485,7 +498,6 @@ const capitalize = token => token[1].toUpperCase();
498 [
499 'xlink:actuate',
500 'xlink:arcrole',
488 - 'xlink:href',
501 'xlink:role',
502 'xlink:show',
503 'xlink:title',
@@ -502,6 +514,7 @@ const capitalize = token => token[1].toUpperCase();
514 false, // mustUseProperty
515 attributeName,
516 'http://www.w3.org/1999/xlink',
517 + false, // sanitizeURL
518 );
519 });
520
@@ -522,6 +535,7 @@ const capitalize = token => token[1].toUpperCase();
535 false, // mustUseProperty
536 attributeName,
537 'http://www.w3.org/XML/1998/namespace',
538 + false, // sanitizeURL
539 );
540 });
541
@@ -535,5 +549,29 @@ const capitalize = token => token[1].toUpperCase();
549 false, // mustUseProperty
550 attributeName.toLowerCase(), // attributeName
551 null, // attributeNamespace
552 + false, // sanitizeURL
553 + );
554 +});
555 +
556 +// These attributes accept URLs. These must not allow javascript: URLS.
557 +// These will also need to accept Trusted Types object in the future.
558 +const xlinkHref = 'xlinkHref';
559 +properties[xlinkHref] = new PropertyInfoRecord(
560 + 'xlinkHref',
561 + STRING,
562 + false, // mustUseProperty
563 + 'xlink:href',
564 + 'http://www.w3.org/1999/xlink',
565 + true, // sanitizeURL
566 +);
567 +
568 +['src', 'href', 'action', 'formAction'].forEach(attributeName => {
569 + properties[attributeName] = new PropertyInfoRecord(
570 + attributeName,
571 + STRING,
572 + false, // mustUseProperty
573 + attributeName.toLowerCase(), // attributeName
574 + null, // attributeNamespace
575 + true, // sanitizeURL
576 );
577 });
packages/react-dom/src/shared/sanitizeURL.js new
+53
@@ -0,0 +1,53 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 invariant from 'shared/invariant';
11 +import warning from 'shared/warning';
12 +import ReactSharedInternals from 'shared/ReactSharedInternals';
13 +import {disableJavaScriptURLs} from 'shared/ReactFeatureFlags';
14 +
15 +let ReactDebugCurrentFrame = ((null: any): {getStackAddendum(): string});
16 +if (__DEV__) {
17 + ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
18 +}
19 +
20 +// A javascript: URL can contain leading C0 control or \u0020 SPACE,
21 +// and any newline or tab are filtered out as if they're not part of the URL.
22 +// https://url.spec.whatwg.org/#url-parsing
23 +// Tab or newline are defined as \r\n\t:
24 +// https://infra.spec.whatwg.org/#ascii-tab-or-newline
25 +// A C0 control is a code point in the range \u0000 NULL to \u001F
26 +// INFORMATION SEPARATOR ONE, inclusive:
27 +// https://infra.spec.whatwg.org/#c0-control-or-space
28 +
29 +/* eslint-disable max-len */
30 +const isJavaScriptProtocol = /^[\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;
31 +
32 +let didWarn = false;
33 +
34 +function sanitizeURL(url: string) {
35 + if (disableJavaScriptURLs) {
36 + invariant(
37 + !isJavaScriptProtocol.test(url),
38 + 'React has blocked a javascript: URL as a security precaution.%s',
39 + __DEV__ ? ReactDebugCurrentFrame.getStackAddendum() : '',
40 + );
41 + } else if (__DEV__ && !didWarn && isJavaScriptProtocol.test(url)) {
42 + didWarn = true;
43 + warning(
44 + false,
45 + 'A future version of React will block javascript: URLs as a security precaution. ' +
46 + 'Use event handlers instead if you can. If you need to generate unsafe HTML try ' +
47 + 'using dangerouslySetInnerHTML instead. React was passed %s.',
48 + JSON.stringify(url),
49 + );
50 + }
51 +}
52 +
53 +export default sanitizeURL;
packages/shared/ReactFeatureFlags.js
+3
@@ -42,6 +42,9 @@ export function addUserTimingListener() {
42 throw new Error('Not implemented.');
43 }
44
45 +// Disable javascript: URL strings in href for XSS protection.
46 +export const disableJavaScriptURLs = false;
47 +
48 // React Fire: prevent the value and checked attributes from syncing
49 // with their related DOM properties
50 export const disableInputAttributeSyncing = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -24,6 +24,7 @@ export const enableStableConcurrentModeAPIs = false;
24 export const warnAboutShorthandPropertyCollision = false;
25 export const enableSchedulerDebugging = false;
26 export const debugRenderPhaseSideEffectsForStrictMode = true;
27 +export const disableJavaScriptURLs = false;
28 export const disableInputAttributeSyncing = false;
29 export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
30 export const warnAboutDeprecatedLifecycles = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -20,6 +20,7 @@ export const warnAboutDeprecatedLifecycles = false;
20 export const enableProfilerTimer = __PROFILE__;
21 export const enableSchedulerTracing = __PROFILE__;
22 export const enableSuspenseServerRenderer = false;
23 +export const disableJavaScriptURLs = false;
24 export const disableInputAttributeSyncing = false;
25 export const enableStableConcurrentModeAPIs = false;
26 export const warnAboutShorthandPropertyCollision = false;
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -20,6 +20,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
20 export const enableProfilerTimer = __PROFILE__;
21 export const enableSchedulerTracing = __PROFILE__;
22 export const enableSuspenseServerRenderer = false;
23 +export const disableJavaScriptURLs = false;
24 export const disableInputAttributeSyncing = false;
25 export const enableStableConcurrentModeAPIs = false;
26 export const warnAboutShorthandPropertyCollision = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -20,6 +20,7 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
20 export const enableProfilerTimer = false;
21 export const enableSchedulerTracing = false;
22 export const enableSuspenseServerRenderer = false;
23 +export const disableJavaScriptURLs = false;
24 export const disableInputAttributeSyncing = false;
25 export const enableStableConcurrentModeAPIs = false;
26 export const warnAboutShorthandPropertyCollision = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -23,6 +23,7 @@ export const enableSuspenseServerRenderer = false;
23 export const enableStableConcurrentModeAPIs = false;
24 export const enableSchedulerDebugging = false;
25 export const warnAboutDeprecatedSetNativeProps = false;
26 +export const disableJavaScriptURLs = false;
27
28 // Only used in www builds.
29 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -16,6 +16,7 @@ export const {
16 debugRenderPhaseSideEffectsForStrictMode,
17 replayFailedUnitOfWorkWithInvokeGuardedCallback,
18 warnAboutDeprecatedLifecycles,
19 + disableJavaScriptURLs,
20 disableInputAttributeSyncing,
21 warnAboutShorthandPropertyCollision,
22 warnAboutDeprecatedSetNativeProps,