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
+});