Skip to content

Commit

Permalink
[FEATURE] Use CSRF like tokens
Browse files Browse the repository at this point in the history
The use of the token is intended to prevent CSRF attacks.

As a positive side effect, it also prevents (accidental) repeated sending. This occurs on iPhones,
for example, when the browser is reopened and the success page was still open in a tab)

See: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Authentication/CSRFlikeRequestTokenHandling.html

Related: #286
Related: #1195
  • Loading branch information
julianhofmann committed Dec 5, 2024
1 parent ec096b4 commit 55f1e66
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 0 deletions.
44 changes: 44 additions & 0 deletions Classes/Controller/FormController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
use Throwable;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException;
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\SecurityAspect;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Core\Security\RequestToken;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Annotation as ExtbaseAnnotation;
Expand Down Expand Up @@ -82,12 +85,14 @@ public function formAction(): ResponseInterface
);
$form = $event->getForm();
SessionUtility::saveFormStartInSession($this->settings, $form);
$requestToken = RequestToken::create('powermail/create');
$this->view->assignMultiple(
[
'form' => $form,
'ttContentData' => $this->contentObject->data,
'messageClass' => $this->messageClass,
'action' => ($this->settings['main']['confirmation'] ? 'checkConfirmation' : 'checkCreate'),
'requestToken' => $requestToken,
]
);

Expand Down Expand Up @@ -262,6 +267,35 @@ public function createAction(Mail $mail, string $hash = ''): ResponseInterface
if ($mail->getUid() !== null && !HashUtility::isHashValid($hash, $mail)) {
return (new ForwardResponse('form'))->withoutArguments();
}
$context = GeneralUtility::makeInstance(Context::class);
$securityAspect = SecurityAspect::provideIn($context);
$requestToken = $securityAspect->getReceivedRequestToken();

if ($requestToken === null) {
$this->addFlashMessage(
LocalizationUtility::translate('error_requesttoken_missing'),
\TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR

Check failure on line 277 in Classes/Controller/FormController.php

View workflow job for this annotation

GitHub Actions / PHPstan

Parameter #2 $messageTitle of method TYPO3\CMS\Extbase\Mvc\Controller\ActionController::addFlashMessage() expects string, TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR given.
);
$this->messageClass = 'error';
return (new ForwardResponse('form'))->withArguments(['messageClass' => $this->messageClass]);
}
if ($requestToken === false) {
$this->addFlashMessage(
LocalizationUtility::translate('error_requesttoken_not_verified'),
\TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR

Check failure on line 285 in Classes/Controller/FormController.php

View workflow job for this annotation

GitHub Actions / PHPstan

Parameter #2 $messageTitle of method TYPO3\CMS\Extbase\Mvc\Controller\ActionController::addFlashMessage() expects string, TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR given.
);
$this->messageClass = 'error';
return (new ForwardResponse('form'))->withArguments(['messageClass' => $this->messageClass]);
}
if ($requestToken->scope !== 'powermail/create') {
$this->addFlashMessage(
LocalizationUtility::translate('error_requesttoken_wrong_scope'),
\TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR

Check failure on line 293 in Classes/Controller/FormController.php

View workflow job for this annotation

GitHub Actions / PHPstan

Parameter #2 $messageTitle of method TYPO3\CMS\Extbase\Mvc\Controller\ActionController::addFlashMessage() expects string, TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR given.
);
$this->messageClass = 'error';
return (new ForwardResponse('form'))->withArguments(['messageClass' => $this->messageClass]);
}

$event = GeneralUtility::makeInstance(FormControllerCreateActionBeforeRenderViewEvent::class, $mail, $hash, $this);
$this->eventDispatcher->dispatch($event);
$mail = $event->getMail();
Expand Down Expand Up @@ -322,6 +356,14 @@ public function createAction(Mail $mail, string $hash = ''): ResponseInterface
$this->contentObject
);

// The middleware takes care to remove the cookie in case no other
// nonce value shall be emitted during the current HTTP request
if ($requestToken->getSigningSecretIdentifier() !== null) {
$securityAspect->getSigningSecretResolver()->revokeIdentifier(
$requestToken->getSigningSecretIdentifier(),
);
}

return $this->htmlResponse();
}

Expand Down Expand Up @@ -372,6 +414,7 @@ protected function sendMailPreflight(Mail $mail, string $hash = ''): void
*/
protected function prepareOutput(Mail $mail): void
{
$requestToken = RequestToken::create('powermail/create');
$this->view->assignMultiple(
[
'variablesWithMarkers' => $this->mailRepository->getVariablesWithMarkersFromMail($mail, true),
Expand All @@ -382,6 +425,7 @@ protected function prepareOutput(Mail $mail): void
'uploadService' => $this->uploadService,
'powermail_rte' => $this->settings['thx']['body'],
'powermail_all' => TemplateUtility::powermailAll($mail, 'web', $this->settings, $this->actionMethodName),
'requestToken' => $requestToken,
]
);
$this->view->assignMultiple($this->mailRepository->getVariablesWithMarkersFromMail($mail, true));
Expand Down
9 changes: 9 additions & 0 deletions Resources/Private/Language/locallang.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@
<trans-unit id="error_mail_not_deleted" resname="error_mail_not_deleted">
<source>Mail could not be deleted. Wrong link or mail was probably already removed from the system.</source>
</trans-unit>
<trans-unit id="error_requesttoken_missing" resname="error_requesttoken_missing">
<source>No request token was provided in the request.</source>
</trans-unit>
<trans-unit id="error_requesttoken_not_verified" resname="error_requesttoken_not_verified">
<source>The given request token could not be verified with the nonce.</source>
</trans-unit>
<trans-unit id="error_requesttoken_wrong_scope" resname="error_requesttoken_wrong_scope">
<source>The given request token seems to be for a different scope.</source>
</trans-unit>
<trans-unit id="validationerror_mandatory" resname="validationerror_mandatory">
<source>This field must be filled!</source>
</trans-unit>
Expand Down
1 change: 1 addition & 0 deletions Resources/Private/Templates/Form/Confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ <h1><f:translate key="confirmation_message" /></h1>
</f:comment>
<f:form
action="checkCreate"
requestToken="{requestToken}"
section="c{ttContentData.uid}"
name="field"
enctype="multipart/form-data"
Expand Down
1 change: 1 addition & 0 deletions Resources/Private/Templates/Form/Form.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<div class="container-fluid">
<f:form
action="{action}"
requestToken="{requestToken}"
section="c{ttContentData.uid}"
name="field"
enctype="multipart/form-data"
Expand Down

0 comments on commit 55f1e66

Please sign in to comment.