/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment */ /* eslint-disable no-script-url */ 'use strict'; const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); let React; let ReactDOMClient; let ReactDOMServer; let ReactTestUtils; let act; const EXPECTED_SAFE_URL = "javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')"; describe('ReactDOMServerIntegration - Untrusted URLs', () => { // The `itRenders` helpers don't work with the gate pragma, so we have to do // this instead. if (gate(flags => flags.disableJavaScriptURLs)) { it("empty test so Jest doesn't complain", () => {}); return; } function initModules() { React = require('react'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); act = require('internal-test-utils').act; // Make them available to the helpers. return { ReactDOMClient, ReactDOMServer, ReactTestUtils, }; } const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules); beforeEach(() => { resetModules(); }); itRenders('a http link with the word javascript in it', async render => { const e = await render( Click me, ); expect(e.tagName).toBe('A'); expect(e.href).toBe('http://javascript:0/thisisfine'); }); itRenders('a javascript protocol href', async render => { // Only the first one warns. The second warning is deduped. const e = await render(
, 1, ); expect(e.firstChild.href).toBe('javascript:notfine'); expect(e.lastChild.href).toBe('javascript:notfineagain'); }); itRenders('a javascript protocol with leading spaces', async render => { const e = await render( p0wned, 1, ); // We use an approximate comparison here because JSDOM might not parse // \u0000 in HTML properly. expect(e.href).toContain('notfine'); }); itRenders( 'a javascript protocol with intermediate new lines and mixed casing', async render => { const e = await render( p0wned, 1, ); expect(e.href).toBe('javascript:notfine'); }, ); itRenders('a javascript protocol area href', async render => { const e = await render( , 1, ); expect(e.firstChild.href).toBe('javascript:notfine'); }); itRenders('a javascript protocol form action', async render => { const e = await render(, 1); expect(e.action).toBe('javascript:notfine'); }); itRenders('a javascript protocol input formAction', async render => { const e = await render( , 1, ); expect(e.getAttribute('formAction')).toBe('javascript:notfine'); }); itRenders('a javascript protocol button formAction', async render => { const e = await render( , 1, ); expect(e.getAttribute('formAction')).toBe('javascript:notfine'); }); itRenders('a javascript protocol iframe src', async render => { const e = await render(, 1); expect(e.src).toBe('javascript:notfine'); }); itRenders('a javascript protocol frame src', async render => { const e = await render(, 1, ); expect(e.lastChild.firstChild.src).toBe('javascript:notfine'); }); itRenders('a javascript protocol in an SVG link', async render => { const e = await render( , 1, ); expect(e.firstChild.getAttribute('href')).toBe('javascript:notfine'); }); itRenders( 'a javascript protocol in an SVG link with a namespace', async render => { const e = await render( , 1, ); expect( e.firstChild.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), ).toBe('javascript:notfine'); }, ); it('rejects a javascript protocol href if it is added during an update', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(async () => { root.render(click me); }); await expect(async () => { await act(() => { root.render(click me); }); }).toErrorDev( '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. React was passed "javascript:notfine".\n' + ' in a (at **)', ); }); }); describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', () => { // The `itRenders` helpers don't work with the gate pragma, so we have to do // this instead. if (gate(flags => !flags.disableJavaScriptURLs)) { it("empty test so Jest doesn't complain", () => {}); return; } function initModules() { const ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.disableJavaScriptURLs = true; React = require('react'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); act = require('internal-test-utils').act; // Make them available to the helpers. return { ReactDOMClient, ReactDOMServer, ReactTestUtils, }; } const { resetModules, itRenders, clientRenderOnBadMarkup, clientRenderOnServerString, } = ReactDOMServerIntegrationUtils(initModules); beforeEach(() => { resetModules(); }); itRenders('a http link with the word javascript in it', async render => { const e = await render( Click me, ); expect(e.tagName).toBe('A'); expect(e.href).toBe('http://javascript:0/thisisfine'); }); itRenders('a javascript protocol href', async render => { // Only the first one warns. The second warning is deduped. const e = await render(
, ); expect(e.firstChild.href).toBe(EXPECTED_SAFE_URL); expect(e.lastChild.href).toBe(EXPECTED_SAFE_URL); }); itRenders('a javascript protocol with leading spaces', async render => { const e = await render( p0wned, ); // We use an approximate comparison here because JSDOM might not parse // \u0000 in HTML properly. expect(e.href).toBe(EXPECTED_SAFE_URL); }); itRenders( 'a javascript protocol with intermediate new lines and mixed casing', async render => { const e = await render( p0wned, ); expect(e.href).toBe(EXPECTED_SAFE_URL); }, ); itRenders('a javascript protocol area href', async render => { const e = await render( , ); expect(e.firstChild.href).toBe(EXPECTED_SAFE_URL); }); itRenders('a javascript protocol form action', async render => { const e = await render(
); expect(e.action).toBe(EXPECTED_SAFE_URL); }); itRenders('a javascript protocol input formAction', async render => { const e = await render( , ); expect(e.getAttribute('formAction')).toBe(EXPECTED_SAFE_URL); }); itRenders('a javascript protocol button formAction', async render => { const e = await render( , ); expect(e.getAttribute('formAction')).toBe(EXPECTED_SAFE_URL); }); itRenders('a javascript protocol iframe src', async render => { const e = await render(); expect(e.src).toBe(EXPECTED_SAFE_URL); }); itRenders('a javascript protocol frame src', async render => { const e = await render(
, ); expect(e.lastChild.firstChild.src).toBe(EXPECTED_SAFE_URL); }); itRenders('a javascript protocol in an SVG link', async render => { const e = await render( , ); expect(e.firstChild.getAttribute('href')).toBe(EXPECTED_SAFE_URL); }); itRenders( 'a javascript protocol in an SVG link with a namespace', async render => { const e = await render( , ); expect( e.firstChild.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), ).toBe(EXPECTED_SAFE_URL); }, ); it('rejects a javascript protocol href if it is added during an update', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(click me); }); expect(container.firstChild.href).toBe('http://thisisfine/'); await act(() => { root.render(click me); }); expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL); }); itRenders('only the first invocation of toString', async render => { let expectedToStringCalls = 1; if (render === clientRenderOnBadMarkup) { // It gets called once on the server and once on the client // which happens to share the same object in our test runner. expectedToStringCalls = 2; } if (render === clientRenderOnServerString && __DEV__) { // The hydration validation calls it one extra time. // TODO: It would be good if we only called toString once for // consistency but the code structure makes that hard right now. expectedToStringCalls = 4; } else if (__DEV__) { // Checking for string coercion problems results in double the // toString calls in DEV expectedToStringCalls *= 2; } let toStringCalls = 0; const firstIsSafe = { toString() { // This tries to avoid the validation by pretending to be safe // the first times it is called and then becomes dangerous. toStringCalls++; if (toStringCalls <= expectedToStringCalls) { return 'https://reactjs.org/'; } return 'javascript:notfine'; }, }; const e = await render(); expect(toStringCalls).toBe(expectedToStringCalls); expect(e.href).toBe('https://reactjs.org/'); }); it('rejects a javascript protocol href if it is added during an update twice', async () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(async () => { root.render(click me); }); expect(container.firstChild.href).toBe('http://thisisfine/'); await act(async () => { root.render(click me); }); expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL); // The second update ensures that a global flag hasn't been added to the regex // which would fail to match the second time it is called. await act(async () => { root.render(click me); }); expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL); }); });