main
js 256 lines 8.18 KB
Raw
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 * @emails react-core
8 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
9 */
10
11 /* eslint-disable no-script-url */
12
13 'use strict';
14
15 const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
16
17 let React;
18 let ReactDOMClient;
19 let ReactDOMServer;
20 let act;
21
22 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 function initModules() {
27 jest.resetModules();
28
29 React = require('react');
30 ReactDOMClient = require('react-dom/client');
31 ReactDOMServer = require('react-dom/server');
32 act = require('internal-test-utils').act;
33
34 // Make them available to the helpers.
35 return {
36 ReactDOMClient,
37 ReactDOMServer,
38 };
39 }
40
41 const {
42 resetModules,
43 itRenders,
44 clientCleanRender,
45 clientRenderOnBadMarkup,
46 clientRenderOnServerString,
47 } = 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 );
69 expect(e.firstChild.href).toBe(EXPECTED_SAFE_URL);
70 expect(e.lastChild.href).toBe(EXPECTED_SAFE_URL);
71 });
72
73 itRenders('sanitizes on various tags', async render => {
74 const aElement = await render(<a href="javascript:notfine" />);
75 expect(aElement.href).toBe(EXPECTED_SAFE_URL);
76
77 const objectElement = await render(<object data="javascript:notfine" />);
78 expect(objectElement.data).toBe(EXPECTED_SAFE_URL);
79
80 const embedElement = await render(<embed src="javascript:notfine" />);
81 expect(embedElement.src).toBe(EXPECTED_SAFE_URL);
82 });
83
84 itRenders('passes through data on non-object tags', async render => {
85 const div = await render(<div data="test" />);
86 expect(div.getAttribute('data')).toBe('test');
87
88 const a = await render(<a data="javascript:fine" />);
89 expect(a.getAttribute('data')).toBe('javascript:fine');
90 });
91
92 itRenders('a javascript protocol with leading spaces', async render => {
93 const e = await render(
94 <a href={' \t \u0000\u001F\u0003javascript\n: notfine'}>p0wned</a>,
95 );
96 // We use an approximate comparison here because JSDOM might not parse
97 // \u0000 in HTML properly.
98 expect(e.href).toBe(EXPECTED_SAFE_URL);
99 });
100
101 itRenders(
102 'a javascript protocol with intermediate new lines and mixed casing',
103 async render => {
104 const e = await render(
105 <a href={'\t\r\n Jav\rasCr\r\niP\t\n\rt\n:notfine'}>p0wned</a>,
106 );
107 expect(e.href).toBe(EXPECTED_SAFE_URL);
108 },
109 );
110
111 itRenders('a javascript protocol area href', async render => {
112 const e = await render(
113 <map>
114 <area href="javascript:notfine" />
115 </map>,
116 );
117 expect(e.firstChild.href).toBe(EXPECTED_SAFE_URL);
118 });
119
120 itRenders('a javascript protocol form action', async render => {
121 const e = await render(<form action="javascript:notfine">p0wned</form>);
122 expect(e.action).toBe(EXPECTED_SAFE_URL);
123 });
124
125 itRenders('a javascript protocol input formAction', async render => {
126 const e = await render(
127 <input type="submit" formAction="javascript:notfine" />,
128 );
129 expect(e.getAttribute('formAction')).toBe(EXPECTED_SAFE_URL);
130 });
131
132 itRenders('a javascript protocol button formAction', async render => {
133 const e = await render(
134 <button formAction="javascript:notfine">p0wned</button>,
135 );
136 expect(e.getAttribute('formAction')).toBe(EXPECTED_SAFE_URL);
137 });
138
139 itRenders('a javascript protocol iframe src', async render => {
140 const e = await render(<iframe src="javascript:notfine" />);
141 expect(e.src).toBe(EXPECTED_SAFE_URL);
142 });
143
144 itRenders('a javascript protocol frame src', async render => {
145 if (render === clientCleanRender || render === clientRenderOnServerString) {
146 // React does not hydrate framesets properly because the default hydration scope
147 // is the body
148 return;
149 }
150 const e = await render(
151 <html>
152 <head />
153 <frameset>
154 <frame src="javascript:notfine" />
155 </frameset>
156 </html>,
157 );
158 expect(e.lastChild.firstChild.src).toBe(EXPECTED_SAFE_URL);
159 });
160
161 itRenders('a javascript protocol in an SVG link', async render => {
162 const e = await render(
163 <svg>
164 <a href="javascript:notfine" />
165 </svg>,
166 );
167 expect(e.firstChild.getAttribute('href')).toBe(EXPECTED_SAFE_URL);
168 });
169
170 itRenders(
171 'a javascript protocol in an SVG link with a namespace',
172 async render => {
173 const e = await render(
174 <svg>
175 <a xlinkHref="javascript:notfine" />
176 </svg>,
177 );
178 expect(
179 e.firstChild.getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
180 ).toBe(EXPECTED_SAFE_URL);
181 },
182 );
183
184 it('rejects a javascript protocol href if it is added during an update', async () => {
185 const container = document.createElement('div');
186 const root = ReactDOMClient.createRoot(container);
187 await act(() => {
188 root.render(<a href="http://thisisfine/">click me</a>);
189 });
190 expect(container.firstChild.href).toBe('http://thisisfine/');
191 await act(() => {
192 root.render(<a href="javascript:notfine">click me</a>);
193 });
194 expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL);
195 });
196
197 itRenders('only the first invocation of toString', async render => {
198 let expectedToStringCalls = 1;
199 if (render === clientRenderOnBadMarkup) {
200 // It gets called once on the server and once on the client
201 // which happens to share the same object in our test runner.
202 expectedToStringCalls = 2;
203 }
204 if (render === clientRenderOnServerString && __DEV__) {
205 // The hydration validation calls it one extra time.
206 // TODO: It would be good if we only called toString once for
207 // consistency but the code structure makes that hard right now.
208 expectedToStringCalls = 4;
209 } else if (__DEV__) {
210 // Checking for string coercion problems results in double the
211 // toString calls in DEV
212 expectedToStringCalls *= 2;
213 }
214
215 if (gate('enableTrustedTypesIntegration') && render === clientCleanRender) {
216 // Trusted types does another toString.
217 expectedToStringCalls += 1;
218 }
219
220 let toStringCalls = 0;
221 const firstIsSafe = {
222 toString() {
223 // This tries to avoid the validation by pretending to be safe
224 // the first times it is called and then becomes dangerous.
225 toStringCalls++;
226 if (toStringCalls <= expectedToStringCalls) {
227 return 'https://reactjs.org/';
228 }
229 return 'javascript:notfine';
230 },
231 };
232
233 const e = await render(<a href={firstIsSafe} />);
234 expect(toStringCalls).toBe(expectedToStringCalls);
235 expect(e.href).toBe('https://reactjs.org/');
236 });
237
238 it('rejects a javascript protocol href if it is added during an update twice', async () => {
239 const container = document.createElement('div');
240 const root = ReactDOMClient.createRoot(container);
241 await act(async () => {
242 root.render(<a href="http://thisisfine/">click me</a>);
243 });
244 expect(container.firstChild.href).toBe('http://thisisfine/');
245 await act(async () => {
246 root.render(<a href="javascript:notfine">click me</a>);
247 });
248 expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL);
249 // The second update ensures that a global flag hasn't been added to the regex
250 // which would fail to match the second time it is called.
251 await act(async () => {
252 root.render(<a href="javascript:notfine">click me</a>);
253 });
254 expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL);
255 });
256 });