diff --git a/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js b/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js index 8f874e2cdb8c4..b186599ca17ec 100644 --- a/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js +++ b/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js @@ -14,11 +14,36 @@ import type {EventSystemFlags} from '../EventSystemFlags'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions'; +import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags'; import {getFiberCurrentPropsFromNode} from '../../client/ReactDOMComponentTree'; import {startHostTransition} from 'react-reconciler/src/ReactFiberReconciler'; +import sanitizeURL from 'react-dom-bindings/src/shared/sanitizeURL'; +import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import {SyntheticEvent} from '../SyntheticEvent'; +function coerceFormActionProp( + actionProp: mixed, +): string | (FormData => void | Promise) | null { + // This should match the logic in ReactDOMComponent + if ( + actionProp == null || + typeof actionProp === 'symbol' || + typeof actionProp === 'boolean' + ) { + return null; + } else if (typeof actionProp === 'function') { + return (actionProp: any); + } else { + if (__DEV__) { + checkAttributeStringCoercion(actionProp, 'action'); + } + return (sanitizeURL( + enableTrustedTypesIntegration ? actionProp : '' + (actionProp: any), + ): any); + } +} + /** * This plugin invokes action functions on forms, inputs and buttons if * the form doesn't prevent default. @@ -48,10 +73,12 @@ function extractEvents( let submitterAction; if (submitter) { const submitterProps = getFiberCurrentPropsFromNode(submitter); - submitterAction = submitterProps - ? (submitterProps: any).formAction - : submitter.getAttribute('formAction'); - if (submitterAction != null) { + submitterAction = coerceFormActionProp( + submitterProps + ? (submitterProps: any).formAction + : submitter.getAttribute('formAction'), + ); + if (submitterAction !== null) { // The submitter overrides the form action. action = submitterAction; // If the action is a function, we don't want to pass its name diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 5db3b2fd8a5be..b8ca871c9b14b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -35,6 +35,7 @@ describe('ReactDOMForm', () => { let ReactDOMClient; let Scheduler; let assertLog; + let assertConsoleErrorDev; let waitForThrow; let useState; let Suspense; @@ -54,6 +55,8 @@ describe('ReactDOMForm', () => { act = require('internal-test-utils').act; assertLog = require('internal-test-utils').assertLog; waitForThrow = require('internal-test-utils').waitForThrow; + assertConsoleErrorDev = + require('internal-test-utils').assertConsoleErrorDev; useState = React.useState; Suspense = React.Suspense; startTransition = React.startTransition; @@ -1782,4 +1785,48 @@ describe('ReactDOMForm', () => { // The form was reset even though the action didn't finish. expect(inputRef.current.value).toBe('Initial'); }); + + test("regression: submitter's formAction prop is coerced correctly before checking if it exists", async () => { + function App({submitterAction}) { + return ( +
Scheduler.log('Form action')}> +