CVE-2021-32697: Improper Input Validation (CWE-20)
value: neos/forms is an open source framework to build web forms. By crafting a special `GET` request containing a valid form state, a form can be submitted without invoking any validators. Form state is secured with an HMAC that is still verified. That means that this issue can only be exploited if Form Finishers cause side effects even if no form values have been sent. Form Finishers can be adjusted in a way that they only execute an action if the submitted form contains some expected data. Alternatively a custom Finisher can be added as first finisher. This regression was introduced with https://github.com/neos/form/commit/049d415295be8d4a0478ccba97dba1bb81649567
CWE description: The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
CVSS v3 base score: 5.3
Published: 2021-06-21T19:15Z
Repository: https://github.com/neos/form
Language: PHP
Files changed: Classes/TYPO3/Form/Core/Runtime/FormRuntime.php, Tests/Functional/SimpleFormTest.php
Commit date: 2013-10-23 20:36:55 +0200
Commit message: [BUGFIX] fix reconstituting a form from its internal state

This is an edge case which is only exposed when persisting the formState
in e.g. a database, and accessing it with GET requests as well.

Change-Id: Ib3d24628f10160f2c170d01fc3daab873595a292
Releases: master

Diff: diff --git a/Classes/TYPO3/Form/Core/Runtime/FormRuntime.php b/Classes/TYPO3/Form/Core/Runtime/FormRuntime.php
index 6e78d96..ca5c051 100644
--- a/Classes/TYPO3/Form/Core/Runtime/FormRuntime.php
+++ b/Classes/TYPO3/Form/Core/Runtime/FormRuntime.php
@@ -144,11 +144,11 @@ class FormRuntime implements \TYPO3\Form\Core\Model\Renderable\RootRenderableInt
 	 */
 	public function initializeObject() {
 		$this->initializeFormStateFromRequest();
 		$this->initializeCurrentPageFromRequest();
 
-		if (!$this->isFirstRequest()) {
+		if (!$this->isFirstRequest() && $this->getRequest()->getHttpRequest()->getMethod() === 'POST') {
 			$this->processSubmittedFormValues();
 		}
 	}
 
 	/**
diff --git a/Tests/Functional/SimpleFormTest.php b/Tests/Functional/SimpleFormTest.php
index d17f8b3..d7d5825 100644
--- a/Tests/Functional/SimpleFormTest.php
+++ b/Tests/Functional/SimpleFormTest.php
@@ -62,17 +62,64 @@ class SimpleFormTest extends AbstractFunctionalTestCase {
 
 		$form = $this->browser->getForm();
 		$form['--three-page-form-with-validation']['text2-1']->setValue('My Text on the second page');
 		$this->gotoPreviousFormPage($form);
 		$this->gotoNextFormPage($this->browser->getForm());
-		$r = $this->gotoNextFormPage($this->browser->getForm());
+		$this->gotoNextFormPage($this->browser->getForm());
 
 		$this->assertSame(' error', $this->browser->getCrawler()->filterXPath('//*[contains(@class,"error")]//input[@id="three-page-form-with-validation-text2-1"]')->attr('class'));
 		$form = $this->browser->getForm();
 		$form['--three-page-form-with-validation']['text2-1']->setValue('42');
 		$this->gotoNextFormPage($form);
 
 		$form = $this->browser->getForm();
 		$this->assertSame('', $form['--three-page-form-with-validation']['text3-1']->getValue());
 	}
 
+
+	/**
+	 * This is an edge-case which occurs if somebody makes the formState persistent, which can happen when subclassing the FormRuntime.
+	 *
+	 * The goal is to build a GET request *only* containing the form state, and nothing else. Furthermore, we need to make sure
+	 * that we do NOT send any of the parameters with the form; as we only want the form state to be applied.
+	 *
+	 * So, if the form state contains some values, we want to be sure these values are re-displayed.
+	 *
+	 * @test
+	 */
+	public function goingForthAndBackStoresFormValuesOfSecondPageEvenWhenSecondPageIsManuallyCalledAsGetRequest() {
+		// 1. TEST SETUP: FORM STATE PREPARATION
+		// - go to the 2nd page of the form, and fill in text2-1.
+		$this->browser->request('http://localhost/test/form/simpleform/ThreePageFormWithValidation');
+
+		$this->gotoNextFormPage($this->browser->getForm());
+
+		$form = $this->browser->getForm();
+		$form['--three-page-form-with-validation']['text2-1']->setValue('My Text on the second page');
+
+		// - then, go back and forth, in order to get an *up-to-date* form state having the right values inside.
+		$this->gotoPreviousFormPage($form);
+		$this->gotoNextFormPage($this->browser->getForm());
+
+		// 2. TEST SETUP: BUILD GET REQUEST ONLY CONTAINING FORM STATE
+		$form = $this->browser->getForm();
+		\TYPO3\Flow\Reflection\ObjectAccess::setProperty($form, 'method', 'GET', TRUE);
+
+		// we want to stay on the current page, that's why we send __currentPage = 1. (== 2nd page of the form)
+		$doc = new \DOMDocument();
+		$doc->loadXML('<input type="hidden" name="--three-page-form-with-validation[__currentPage]" value="1" />');
+		$node = $doc->getElementsByTagName('input')->item(0);
+		$form->set(new InputFormField($node));
+
+		// We do *not* send any form content with us, as we want to test these are properly reconstituted from the form state.
+		$form->offsetUnset('--three-page-form-with-validation[text2-1]');
+
+		// 3. TEST RUN
+		// submit the GET request ONLY containing formState.
+		$this->browser->submit($form);
+
+		// now, make sure the text2-1 (which has been persisted in the form state) gets reconstituted and shown properly.
+		$form = $this->browser->getForm();
+		$this->assertSame('My Text on the second page', $form['--three-page-form-with-validation']['text2-1']->getValue());
+	}
+
 }

Vulnerable code: 		if (!$this->isFirstRequest()) {
		$r = $this->gotoNextFormPage($this->browser->getForm());
Fixed code: 		if (!$this->isFirstRequest() && $this->getRequest()->getHttpRequest()->getMethod() === 'POST') {
		$this->gotoNextFormPage($this->browser->getForm());

	/**
	 * This is an edge-case which occurs if somebody makes the formState persistent, which can happen when subclassing the FormRuntime.
	 *
	 * The goal is to build a GET request *only* containing the form state, and nothing else. Furthermore, we need to make sure
	 * that we do NOT send any of the parameters with the form; as we only want the form state to be applied.
	 *
	 * So, if the form state contains some values, we want to be sure these values are re-displayed.
	 *
	 * @test
	 */
	public function goingForthAndBackStoresFormValuesOfSecondPageEvenWhenSecondPageIsManuallyCalledAsGetRequest() {
		// 1. TEST SETUP: FORM STATE PREPARATION
		// - go to the 2nd page of the form, and fill in text2-1.
		$this->browser->request('http://localhost/test/form/simpleform/ThreePageFormWithValidation');

		$this->gotoNextFormPage($this->browser->getForm());

		$form = $this->browser->getForm();
		$form['--three-page-form-with-validation']['text2-1']->setValue('My Text on the second page');

		// - then, go back and forth, in order to get an *up-to-date* form state having the right values inside.
		$this->gotoPreviousFormPage($form);
		$this->gotoNextFormPage($this->browser->getForm());

		// 2. TEST SETUP: BUILD GET REQUEST ONLY CONTAINING FORM STATE
		$form = $this->browser->getForm();
		\TYPO3\Flow\Reflection\ObjectAccess::setProperty($form, 'method', 'GET', TRUE);

		// we want to stay on the current page, that's why we send __currentPage = 1. (== 2nd page of the form)
		$doc = new \DOMDocument();
		$doc->loadXML('<input type="hidden" name="--three-page-form-with-validation[__currentPage]" value="1" />');
		$node = $doc->getElementsByTagName('input')->item(0);
		$form->set(new InputFormField($node));

		// We do *not* send any form content with us, as we want to test these are properly reconstituted from the form state.
		$form->offsetUnset('--three-page-form-with-validation[text2-1]');

		// 3. TEST RUN
		// submit the GET request ONLY containing formState.
		$this->browser->submit($form);

		// now, make sure the text2-1 (which has been persisted in the form state) gets reconstituted and shown properly.
		$form = $this->browser->getForm();
		$this->assertSame('My Text on the second page', $form['--three-page-form-with-validation']['text2-1']->getValue());
	}