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 ReactDOM;
    
  19. let ReactDOMServer;
    
  20. let ReactTestUtils;
    
  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.   // 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.     ReactDOM = require('react-dom');
    
  37.     ReactDOMServer = require('react-dom/server');
    
  38.     ReactTestUtils = require('react-dom/test-utils');
    
  39. 
    
  40.     // Make them available to the helpers.
    
  41.     return {
    
  42.       ReactDOM,
    
  43.       ReactDOMServer,
    
  44.       ReactTestUtils,
    
  45.     };
    
  46.   }
    
  47. 
    
  48.   const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
    
  49. 
    
  50.   beforeEach(() => {
    
  51.     resetModules();
    
  52.   });
    
  53. 
    
  54.   itRenders('a http link with the word javascript in it', async render => {
    
  55.     const e = await render(
    
  56.       <a href="http://javascript:0/thisisfine">Click me</a>,
    
  57.     );
    
  58.     expect(e.tagName).toBe('A');
    
  59.     expect(e.href).toBe('http://javascript:0/thisisfine');
    
  60.   });
    
  61. 
    
  62.   itRenders('a javascript protocol href', async render => {
    
  63.     // Only the first one warns. The second warning is deduped.
    
  64.     const e = await render(
    
  65.       <div>
    
  66.         <a href="javascript:notfine">p0wned</a>
    
  67.         <a href="javascript:notfineagain">p0wned again</a>
    
  68.       </div>,
    
  69.       1,
    
  70.     );
    
  71.     expect(e.firstChild.href).toBe('javascript:notfine');
    
  72.     expect(e.lastChild.href).toBe('javascript:notfineagain');
    
  73.   });
    
  74. 
    
  75.   itRenders('a javascript protocol with leading spaces', async render => {
    
  76.     const e = await render(
    
  77.       <a href={'  \t \u0000\u001F\u0003javascript\n: notfine'}>p0wned</a>,
    
  78.       1,
    
  79.     );
    
  80.     // We use an approximate comparison here because JSDOM might not parse
    
  81.     // \u0000 in HTML properly.
    
  82.     expect(e.href).toContain('notfine');
    
  83.   });
    
  84. 
    
  85.   itRenders(
    
  86.     'a javascript protocol with intermediate new lines and mixed casing',
    
  87.     async render => {
    
  88.       const e = await render(
    
  89.         <a href={'\t\r\n Jav\rasCr\r\niP\t\n\rt\n:notfine'}>p0wned</a>,
    
  90.         1,
    
  91.       );
    
  92.       expect(e.href).toBe('javascript:notfine');
    
  93.     },
    
  94.   );
    
  95. 
    
  96.   itRenders('a javascript protocol area href', async render => {
    
  97.     const e = await render(
    
  98.       <map>
    
  99.         <area href="javascript:notfine" />
    
  100.       </map>,
    
  101.       1,
    
  102.     );
    
  103.     expect(e.firstChild.href).toBe('javascript:notfine');
    
  104.   });
    
  105. 
    
  106.   itRenders('a javascript protocol form action', async render => {
    
  107.     const e = await render(<form action="javascript:notfine">p0wned</form>, 1);
    
  108.     expect(e.action).toBe('javascript:notfine');
    
  109.   });
    
  110. 
    
  111.   itRenders('a javascript protocol input formAction', async render => {
    
  112.     const e = await render(
    
  113.       <input type="submit" formAction="javascript:notfine" />,
    
  114.       1,
    
  115.     );
    
  116.     expect(e.getAttribute('formAction')).toBe('javascript:notfine');
    
  117.   });
    
  118. 
    
  119.   itRenders('a javascript protocol button formAction', async render => {
    
  120.     const e = await render(
    
  121.       <button formAction="javascript:notfine">p0wned</button>,
    
  122.       1,
    
  123.     );
    
  124.     expect(e.getAttribute('formAction')).toBe('javascript:notfine');
    
  125.   });
    
  126. 
    
  127.   itRenders('a javascript protocol iframe src', async render => {
    
  128.     const e = await render(<iframe src="javascript:notfine" />, 1);
    
  129.     expect(e.src).toBe('javascript:notfine');
    
  130.   });
    
  131. 
    
  132.   itRenders('a javascript protocol frame src', async render => {
    
  133.     const e = await render(
    
  134.       <html>
    
  135.         <head />
    
  136.         <frameset>
    
  137.           <frame src="javascript:notfine" />
    
  138.         </frameset>
    
  139.       </html>,
    
  140.       1,
    
  141.     );
    
  142.     expect(e.lastChild.firstChild.src).toBe('javascript:notfine');
    
  143.   });
    
  144. 
    
  145.   itRenders('a javascript protocol in an SVG link', async render => {
    
  146.     const e = await render(
    
  147.       <svg>
    
  148.         <a href="javascript:notfine" />
    
  149.       </svg>,
    
  150.       1,
    
  151.     );
    
  152.     expect(e.firstChild.getAttribute('href')).toBe('javascript:notfine');
    
  153.   });
    
  154. 
    
  155.   itRenders(
    
  156.     'a javascript protocol in an SVG link with a namespace',
    
  157.     async render => {
    
  158.       const e = await render(
    
  159.         <svg>
    
  160.           <a xlinkHref="javascript:notfine" />
    
  161.         </svg>,
    
  162.         1,
    
  163.       );
    
  164.       expect(
    
  165.         e.firstChild.getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
    
  166.       ).toBe('javascript:notfine');
    
  167.     },
    
  168.   );
    
  169. 
    
  170.   it('rejects a javascript protocol href if it is added during an update', () => {
    
  171.     const container = document.createElement('div');
    
  172.     ReactDOM.render(<a href="thisisfine">click me</a>, container);
    
  173.     expect(() => {
    
  174.       ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
    
  175.     }).toErrorDev(
    
  176.       'Warning: A future version of React will block javascript: URLs as a security precaution. ' +
    
  177.         'Use event handlers instead if you can. If you need to generate unsafe HTML try using ' +
    
  178.         'dangerouslySetInnerHTML instead. React was passed "javascript:notfine".\n' +
    
  179.         '    in a (at **)',
    
  180.     );
    
  181.   });
    
  182. });
    
  183. 
    
  184. describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', () => {
    
  185.   // The `itRenders` helpers don't work with the gate pragma, so we have to do
    
  186.   // this instead.
    
  187.   if (gate(flags => !flags.disableJavaScriptURLs)) {
    
  188.     it("empty test so Jest doesn't complain", () => {});
    
  189.     return;
    
  190.   }
    
  191. 
    
  192.   function initModules() {
    
  193.     jest.resetModules();
    
  194.     const ReactFeatureFlags = require('shared/ReactFeatureFlags');
    
  195.     ReactFeatureFlags.disableJavaScriptURLs = true;
    
  196. 
    
  197.     React = require('react');
    
  198.     ReactDOM = require('react-dom');
    
  199.     ReactDOMServer = require('react-dom/server');
    
  200.     ReactTestUtils = require('react-dom/test-utils');
    
  201. 
    
  202.     // Make them available to the helpers.
    
  203.     return {
    
  204.       ReactDOM,
    
  205.       ReactDOMServer,
    
  206.       ReactTestUtils,
    
  207.     };
    
  208.   }
    
  209. 
    
  210.   const {
    
  211.     resetModules,
    
  212.     itRenders,
    
  213.     clientRenderOnBadMarkup,
    
  214.     clientRenderOnServerString,
    
  215.   } = ReactDOMServerIntegrationUtils(initModules);
    
  216. 
    
  217.   beforeEach(() => {
    
  218.     resetModules();
    
  219.   });
    
  220. 
    
  221.   itRenders('a http link with the word javascript in it', async render => {
    
  222.     const e = await render(
    
  223.       <a href="http://javascript:0/thisisfine">Click me</a>,
    
  224.     );
    
  225.     expect(e.tagName).toBe('A');
    
  226.     expect(e.href).toBe('http://javascript:0/thisisfine');
    
  227.   });
    
  228. 
    
  229.   itRenders('a javascript protocol href', async render => {
    
  230.     // Only the first one warns. The second warning is deduped.
    
  231.     const e = await render(
    
  232.       <div>
    
  233.         <a href="javascript:notfine">p0wned</a>
    
  234.         <a href="javascript:notfineagain">p0wned again</a>
    
  235.       </div>,
    
  236.     );
    
  237.     expect(e.firstChild.href).toBe(EXPECTED_SAFE_URL);
    
  238.     expect(e.lastChild.href).toBe(EXPECTED_SAFE_URL);
    
  239.   });
    
  240. 
    
  241.   itRenders('a javascript protocol with leading spaces', async render => {
    
  242.     const e = await render(
    
  243.       <a href={'  \t \u0000\u001F\u0003javascript\n: notfine'}>p0wned</a>,
    
  244.     );
    
  245.     // We use an approximate comparison here because JSDOM might not parse
    
  246.     // \u0000 in HTML properly.
    
  247.     expect(e.href).toBe(EXPECTED_SAFE_URL);
    
  248.   });
    
  249. 
    
  250.   itRenders(
    
  251.     'a javascript protocol with intermediate new lines and mixed casing',
    
  252.     async render => {
    
  253.       const e = await render(
    
  254.         <a href={'\t\r\n Jav\rasCr\r\niP\t\n\rt\n:notfine'}>p0wned</a>,
    
  255.       );
    
  256.       expect(e.href).toBe(EXPECTED_SAFE_URL);
    
  257.     },
    
  258.   );
    
  259. 
    
  260.   itRenders('a javascript protocol area href', async render => {
    
  261.     const e = await render(
    
  262.       <map>
    
  263.         <area href="javascript:notfine" />
    
  264.       </map>,
    
  265.     );
    
  266.     expect(e.firstChild.href).toBe(EXPECTED_SAFE_URL);
    
  267.   });
    
  268. 
    
  269.   itRenders('a javascript protocol form action', async render => {
    
  270.     const e = await render(<form action="javascript:notfine">p0wned</form>);
    
  271.     expect(e.action).toBe(EXPECTED_SAFE_URL);
    
  272.   });
    
  273. 
    
  274.   itRenders('a javascript protocol input formAction', async render => {
    
  275.     const e = await render(
    
  276.       <input type="submit" formAction="javascript:notfine" />,
    
  277.     );
    
  278.     expect(e.getAttribute('formAction')).toBe(EXPECTED_SAFE_URL);
    
  279.   });
    
  280. 
    
  281.   itRenders('a javascript protocol button formAction', async render => {
    
  282.     const e = await render(
    
  283.       <button formAction="javascript:notfine">p0wned</button>,
    
  284.     );
    
  285.     expect(e.getAttribute('formAction')).toBe(EXPECTED_SAFE_URL);
    
  286.   });
    
  287. 
    
  288.   itRenders('a javascript protocol iframe src', async render => {
    
  289.     const e = await render(<iframe src="javascript:notfine" />);
    
  290.     expect(e.src).toBe(EXPECTED_SAFE_URL);
    
  291.   });
    
  292. 
    
  293.   itRenders('a javascript protocol frame src', async render => {
    
  294.     const e = await render(
    
  295.       <html>
    
  296.         <head />
    
  297.         <frameset>
    
  298.           <frame src="javascript:notfine" />
    
  299.         </frameset>
    
  300.       </html>,
    
  301.     );
    
  302.     expect(e.lastChild.firstChild.src).toBe(EXPECTED_SAFE_URL);
    
  303.   });
    
  304. 
    
  305.   itRenders('a javascript protocol in an SVG link', async render => {
    
  306.     const e = await render(
    
  307.       <svg>
    
  308.         <a href="javascript:notfine" />
    
  309.       </svg>,
    
  310.     );
    
  311.     expect(e.firstChild.getAttribute('href')).toBe(EXPECTED_SAFE_URL);
    
  312.   });
    
  313. 
    
  314.   itRenders(
    
  315.     'a javascript protocol in an SVG link with a namespace',
    
  316.     async render => {
    
  317.       const e = await render(
    
  318.         <svg>
    
  319.           <a xlinkHref="javascript:notfine" />
    
  320.         </svg>,
    
  321.       );
    
  322.       expect(
    
  323.         e.firstChild.getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
    
  324.       ).toBe(EXPECTED_SAFE_URL);
    
  325.     },
    
  326.   );
    
  327. 
    
  328.   it('rejects a javascript protocol href if it is added during an update', () => {
    
  329.     const container = document.createElement('div');
    
  330.     ReactDOM.render(<a href="http://thisisfine/">click me</a>, container);
    
  331.     expect(container.firstChild.href).toBe('http://thisisfine/');
    
  332.     ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
    
  333.     expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL);
    
  334.   });
    
  335. 
    
  336.   itRenders('only the first invocation of toString', async render => {
    
  337.     let expectedToStringCalls = 1;
    
  338.     if (render === clientRenderOnBadMarkup) {
    
  339.       // It gets called once on the server and once on the client
    
  340.       // which happens to share the same object in our test runner.
    
  341.       expectedToStringCalls = 2;
    
  342.     }
    
  343.     if (render === clientRenderOnServerString && __DEV__) {
    
  344.       // The hydration validation calls it one extra time.
    
  345.       // TODO: It would be good if we only called toString once for
    
  346.       // consistency but the code structure makes that hard right now.
    
  347.       expectedToStringCalls = 4;
    
  348.     } else if (__DEV__) {
    
  349.       // Checking for string coercion problems results in double the
    
  350.       // toString calls in DEV
    
  351.       expectedToStringCalls *= 2;
    
  352.     }
    
  353. 
    
  354.     let toStringCalls = 0;
    
  355.     const firstIsSafe = {
    
  356.       toString() {
    
  357.         // This tries to avoid the validation by pretending to be safe
    
  358.         // the first times it is called and then becomes dangerous.
    
  359.         toStringCalls++;
    
  360.         if (toStringCalls <= expectedToStringCalls) {
    
  361.           return 'https://reactjs.org/';
    
  362.         }
    
  363.         return 'javascript:notfine';
    
  364.       },
    
  365.     };
    
  366. 
    
  367.     const e = await render(<a href={firstIsSafe} />);
    
  368.     expect(toStringCalls).toBe(expectedToStringCalls);
    
  369.     expect(e.href).toBe('https://reactjs.org/');
    
  370.   });
    
  371. 
    
  372.   it('rejects a javascript protocol href if it is added during an update twice', () => {
    
  373.     const container = document.createElement('div');
    
  374.     ReactDOM.render(<a href="http://thisisfine/">click me</a>, container);
    
  375.     expect(container.firstChild.href).toBe('http://thisisfine/');
    
  376.     ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
    
  377.     expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL);
    
  378.     // The second update ensures that a global flag hasn't been added to the regex
    
  379.     // which would fail to match the second time it is called.
    
  380.     ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
    
  381.     expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL);
    
  382.   });
    
  383. });