mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
c826dc50de
This lets you pass a function to `<form action={...}>` or `<button
formAction={...}>` or `<input type="submit formAction={...}>`. This will
behave basically like a `javascript:` URL except not quite implemented
that way. This is a convenience for the `onSubmit={e => {
e.preventDefault(); const fromData = new FormData(e.target); ... }`
pattern.
You can still implement a custom `onSubmit` handler and if it calls
`preventDefault`, it won't invoke the action, just like it would if you
used a full page form navigation or javascript urls. It behaves just
like a navigation and we might implement it with the Navigation API in
the future.
Currently this is just a synchronous function but in a follow up this
will accept async functions, handle pending states and handle errors.
This is implemented by setting `javascript:` URLs, but these only exist
to trigger an error message if something goes wrong instead of
navigating away. Like if you called `stopPropagation` to prevent React
from handling it or if you called `form.submit()` instead of
`form.requestSubmit()` which by-passes the `submit` event. If CSP is
used to ban `javascript:` urls, those will trigger errors when these
URLs are invoked which would be a different error message but it's still
there to notify the user that something went wrong in the plumbing.
Next up is improving the SSR state with action replaying and progressive
enhancement.
384 lines
12 KiB
JavaScript
384 lines
12 KiB
JavaScript
/**
|
|
* 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 ReactDOM;
|
|
let ReactDOMServer;
|
|
let ReactTestUtils;
|
|
|
|
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() {
|
|
jest.resetModules();
|
|
React = require('react');
|
|
ReactDOM = require('react-dom');
|
|
ReactDOMServer = require('react-dom/server');
|
|
ReactTestUtils = require('react-dom/test-utils');
|
|
|
|
// Make them available to the helpers.
|
|
return {
|
|
ReactDOM,
|
|
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(
|
|
<a href="http://javascript:0/thisisfine">Click me</a>,
|
|
);
|
|
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(
|
|
<div>
|
|
<a href="javascript:notfine">p0wned</a>
|
|
<a href="javascript:notfineagain">p0wned again</a>
|
|
</div>,
|
|
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(
|
|
<a href={' \t \u0000\u001F\u0003javascript\n: notfine'}>p0wned</a>,
|
|
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(
|
|
<a href={'\t\r\n Jav\rasCr\r\niP\t\n\rt\n:notfine'}>p0wned</a>,
|
|
1,
|
|
);
|
|
expect(e.href).toBe('javascript:notfine');
|
|
},
|
|
);
|
|
|
|
itRenders('a javascript protocol area href', async render => {
|
|
const e = await render(
|
|
<map>
|
|
<area href="javascript:notfine" />
|
|
</map>,
|
|
1,
|
|
);
|
|
expect(e.firstChild.href).toBe('javascript:notfine');
|
|
});
|
|
|
|
itRenders('a javascript protocol form action', async render => {
|
|
const e = await render(<form action="javascript:notfine">p0wned</form>, 1);
|
|
expect(e.action).toBe('javascript:notfine');
|
|
});
|
|
|
|
itRenders('a javascript protocol input formAction', async render => {
|
|
const e = await render(
|
|
<input type="submit" formAction="javascript:notfine" />,
|
|
1,
|
|
);
|
|
expect(e.getAttribute('formAction')).toBe('javascript:notfine');
|
|
});
|
|
|
|
itRenders('a javascript protocol button formAction', async render => {
|
|
const e = await render(
|
|
<button formAction="javascript:notfine">p0wned</button>,
|
|
1,
|
|
);
|
|
expect(e.getAttribute('formAction')).toBe('javascript:notfine');
|
|
});
|
|
|
|
itRenders('a javascript protocol iframe src', async render => {
|
|
const e = await render(<iframe src="javascript:notfine" />, 1);
|
|
expect(e.src).toBe('javascript:notfine');
|
|
});
|
|
|
|
itRenders('a javascript protocol frame src', async render => {
|
|
const e = await render(
|
|
<html>
|
|
<head />
|
|
<frameset>
|
|
<frame src="javascript:notfine" />
|
|
</frameset>
|
|
</html>,
|
|
1,
|
|
);
|
|
expect(e.lastChild.firstChild.src).toBe('javascript:notfine');
|
|
});
|
|
|
|
itRenders('a javascript protocol in an SVG link', async render => {
|
|
const e = await render(
|
|
<svg>
|
|
<a href="javascript:notfine" />
|
|
</svg>,
|
|
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(
|
|
<svg>
|
|
<a xlinkHref="javascript:notfine" />
|
|
</svg>,
|
|
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', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<a href="thisisfine">click me</a>, container);
|
|
expect(() => {
|
|
ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
|
|
}).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() {
|
|
jest.resetModules();
|
|
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
|
ReactFeatureFlags.disableJavaScriptURLs = true;
|
|
|
|
React = require('react');
|
|
ReactDOM = require('react-dom');
|
|
ReactDOMServer = require('react-dom/server');
|
|
ReactTestUtils = require('react-dom/test-utils');
|
|
|
|
// Make them available to the helpers.
|
|
return {
|
|
ReactDOM,
|
|
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(
|
|
<a href="http://javascript:0/thisisfine">Click me</a>,
|
|
);
|
|
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(
|
|
<div>
|
|
<a href="javascript:notfine">p0wned</a>
|
|
<a href="javascript:notfineagain">p0wned again</a>
|
|
</div>,
|
|
);
|
|
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(
|
|
<a href={' \t \u0000\u001F\u0003javascript\n: notfine'}>p0wned</a>,
|
|
);
|
|
// 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(
|
|
<a href={'\t\r\n Jav\rasCr\r\niP\t\n\rt\n:notfine'}>p0wned</a>,
|
|
);
|
|
expect(e.href).toBe(EXPECTED_SAFE_URL);
|
|
},
|
|
);
|
|
|
|
itRenders('a javascript protocol area href', async render => {
|
|
const e = await render(
|
|
<map>
|
|
<area href="javascript:notfine" />
|
|
</map>,
|
|
);
|
|
expect(e.firstChild.href).toBe(EXPECTED_SAFE_URL);
|
|
});
|
|
|
|
itRenders('a javascript protocol form action', async render => {
|
|
const e = await render(<form action="javascript:notfine">p0wned</form>);
|
|
expect(e.action).toBe(EXPECTED_SAFE_URL);
|
|
});
|
|
|
|
itRenders('a javascript protocol input formAction', async render => {
|
|
const e = await render(
|
|
<input type="submit" formAction="javascript:notfine" />,
|
|
);
|
|
expect(e.getAttribute('formAction')).toBe(EXPECTED_SAFE_URL);
|
|
});
|
|
|
|
itRenders('a javascript protocol button formAction', async render => {
|
|
const e = await render(
|
|
<button formAction="javascript:notfine">p0wned</button>,
|
|
);
|
|
expect(e.getAttribute('formAction')).toBe(EXPECTED_SAFE_URL);
|
|
});
|
|
|
|
itRenders('a javascript protocol iframe src', async render => {
|
|
const e = await render(<iframe src="javascript:notfine" />);
|
|
expect(e.src).toBe(EXPECTED_SAFE_URL);
|
|
});
|
|
|
|
itRenders('a javascript protocol frame src', async render => {
|
|
const e = await render(
|
|
<html>
|
|
<head />
|
|
<frameset>
|
|
<frame src="javascript:notfine" />
|
|
</frameset>
|
|
</html>,
|
|
);
|
|
expect(e.lastChild.firstChild.src).toBe(EXPECTED_SAFE_URL);
|
|
});
|
|
|
|
itRenders('a javascript protocol in an SVG link', async render => {
|
|
const e = await render(
|
|
<svg>
|
|
<a href="javascript:notfine" />
|
|
</svg>,
|
|
);
|
|
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(
|
|
<svg>
|
|
<a xlinkHref="javascript:notfine" />
|
|
</svg>,
|
|
);
|
|
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', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<a href="http://thisisfine/">click me</a>, container);
|
|
expect(container.firstChild.href).toBe('http://thisisfine/');
|
|
ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
|
|
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(<a href={firstIsSafe} />);
|
|
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', () => {
|
|
const container = document.createElement('div');
|
|
ReactDOM.render(<a href="http://thisisfine/">click me</a>, container);
|
|
expect(container.firstChild.href).toBe('http://thisisfine/');
|
|
ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
|
|
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.
|
|
ReactDOM.render(<a href="javascript:notfine">click me</a>, container);
|
|
expect(container.firstChild.href).toBe(EXPECTED_SAFE_URL);
|
|
});
|
|
});
|