diff --git a/src/Core/Form/FormTypeNonceExtension.php b/src/Core/Form/FormTypeNonceExtension.php new file mode 100644 index 0000000000..0d5e5ca804 --- /dev/null +++ b/src/Core/Form/FormTypeNonceExtension.php @@ -0,0 +1,68 @@ +addEventSubscriber($this); + } + + public static function getSubscribedEvents() + { + return [FormEvents::PRE_SUBMIT => 'preSubmit']; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(['nonce_protection' => true]); + } + + /** + * Before the form gets to the controller, piggy-back on the CSRF token and use it as a nonce, ensuring it was only submitted once + */ + public function preSubmit(FormEvent $event) + { + $form = $event->getForm(); + $postRequestSizeExceeded = 'POST' === $form->getConfig()->getMethod() && $this->serverParams->hasPostMaxSizeBeenExceeded(); + + if ($form->isRoot() && $form->getConfig()->getOption('compound') && !$postRequestSizeExceeded) { + $data = $event->getData(); + $token_id = $form->getConfig()->getOption('csrf_token_id') ?: ($form->getName() ?: \get_class($form->getConfig()->getType()->getInnerType())); + $token_value = \is_string($data['_token'] ?? null) ? $data['_token'] : null; + $csrf_token = new CsrfToken($token_id, $token_value); + + if (null === $token_value || !$this->token_manager->isTokenValid($csrf_token) || Cache::incr("nonce:{$token_value}") !== 1) { // TODO add TTL + $form->addError(new FormError(_m('Invalid nonce'), null, [], null, $csrf_token)); + } + } + } +}