diff --git a/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.html b/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.html new file mode 100644 index 0000000000000000000000000000000000000000..4cc40a8586d36ede1e319ce7d48ce1a63864df94 --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.html @@ -0,0 +1,11 @@ +<f:layout name="SystemEmail" /> +<f:section name="Subject">TYPO3 Login Failure Warning (at {typo3.sitename})</f:section> +<f:section name="Title">TYPO3 Login Failure Warning at {typo3.sitename}</f:section> +<f:section name="Main"> + <strong>There have been some attempts ({lines -> f:count()}) to login at the TYPO3 site "{typo3.sitename}" (<strong>{normalizedParams.remoteAddress}</strong>).</strong> + <br><br> + This is a dump of the failures:<br> +<f:for each="{lines}" as="line"> + {f:format.date(date:line.row.tstamp, format:'{typo3.formats.date} {typo3.formats.time}')}: {line.text}<br> +</f:for> +</f:section> diff --git a/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.txt b/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.txt new file mode 100644 index 0000000000000000000000000000000000000000..2247fe612169f38f92645dce0451d2509e99af9d --- /dev/null +++ b/typo3/sysext/backend/Resources/Private/Templates/Email/Security/LoginAttemptFailedWarning.txt @@ -0,0 +1,9 @@ +<f:layout name="SystemEmail" /> +<f:section name="Subject">TYPO3 Login Failure Warning (at {typo3.sitename})</f:section> +<f:section name="Title">TYPO3 Login Failure Warning at {typo3.sitename}</f:section> +<f:section name="Main">There have been some attempts ({lines -> f:count()}) to login at the TYPO3 site "{typo3.sitename}" ({normalizedParams.remoteAddress}). + +This is a dump of the failures: +<f:for each="{lines}" as="line">{f:format.date(date:line.row.tstamp, format:'{typo3.formats.date} {typo3.formats.time}')}: {line.text -> f:format.raw()} +</f:for> +</f:section> diff --git a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php index 97d20ddda7214831b10bfdfeb880011644d7a553..9cb8a2b9d27c7c8a0b249009be46ef219a0cf31d 100644 --- a/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php +++ b/typo3/sysext/core/Classes/Authentication/BackendUserAuthentication.php @@ -14,6 +14,8 @@ namespace TYPO3\CMS\Core\Authentication; * The TYPO3 project - inspiring people to share! */ +use Doctrine\DBAL\Driver\Statement; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Cache\CacheManager; @@ -27,6 +29,8 @@ use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction; use TYPO3\CMS\Core\FormProtection\FormProtectionFactory; +use TYPO3\CMS\Core\Mail\FluidEmail; +use TYPO3\CMS\Core\Mail\Mailer; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction; @@ -2333,91 +2337,101 @@ TCAdefaults.sys_note.email = ' . $this->user['email']; */ public function checkLogFailures($email, $secondsBack = 3600, $max = 3) { - if ($email) { - $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); + if (!GeneralUtility::validEmail($email)) { + return; + } + $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); - // Get last flag set in the log for sending - $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack; - $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log'); - $queryBuilder->select('tstamp') - ->from('sys_log') - ->where( - $queryBuilder->expr()->eq( - 'type', - $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - 'action', - $queryBuilder->createNamedParameter(4, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->gt( - 'tstamp', - $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT) - ) + // Get last flag set in the log for sending + $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack; + $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log'); + $queryBuilder->select('tstamp') + ->from('sys_log') + ->where( + $queryBuilder->expr()->eq( + 'type', + $queryBuilder->createNamedParameter(SystemLogType::LOGIN, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'action', + $queryBuilder->createNamedParameter(SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->gt( + 'tstamp', + $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT) ) - ->orderBy('tstamp', 'DESC') - ->setMaxResults(1); - if ($testRow = $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC)) { - $theTimeBack = $testRow['tstamp']; - } + ) + ->orderBy('tstamp', 'DESC') + ->setMaxResults(1); + if ($testRow = $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC)) { + $theTimeBack = $testRow['tstamp']; + } - $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log'); - $result = $queryBuilder->select('*') - ->from('sys_log') - ->where( - $queryBuilder->expr()->eq( - 'type', - $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - 'action', - $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->neq( - 'error', - $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->gt( - 'tstamp', - $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT) - ) + $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log'); + $result = $queryBuilder->select('*') + ->from('sys_log') + ->where( + $queryBuilder->expr()->eq( + 'type', + $queryBuilder->createNamedParameter(SystemLogType::LOGIN, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'action', + $queryBuilder->createNamedParameter(SystemLogLoginAction::ATTEMPT, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->neq( + 'error', + $queryBuilder->createNamedParameter(SystemLogErrorClassification::MESSAGE, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->gt( + 'tstamp', + $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT) ) - ->orderBy('tstamp') - ->execute(); + ) + ->orderBy('tstamp') + ->execute(); - $rowCount = $queryBuilder - ->count('uid') - ->execute() - ->fetchColumn(0); - // Check for more than $max number of error failures with the last period. - if ($rowCount > $max) { - // OK, so there were more than the max allowed number of login failures - so we will send an email then. - $subject = 'TYPO3 Login Failure Warning (at ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ')'; - $email_body = 'There have been some attempts (' . $rowCount . ') to login at the TYPO3 -site "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '" (' . GeneralUtility::getIndpEnv('HTTP_HOST') . '). - -This is a dump of the failures: - -'; - while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { - $theData = unserialize($row['log_data']); - $text = @sprintf($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]); - if ((int)$row['type'] === 255) { - $text = str_replace('###IP###', $row['IP'], $text); - } - $email_body .= date( - $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], - $row['tstamp'] - ) . ': ' . $text; - $email_body .= LF; - } - $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class); - $mail->setTo($email)->subject($subject)->text($email_body); - $mail->send(); - // Logout written to log - $this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, SystemLogErrorClassification::MESSAGE, 3, 'Failure warning (%s failures within %s seconds) sent by email to %s', [$rowCount, $secondsBack, $email]); + $rowCount = $queryBuilder + ->count('uid') + ->execute() + ->fetchColumn(0); + // Check for more than $max number of error failures with the last period. + if ($rowCount > $max) { + // OK, so there were more than the max allowed number of login failures - so we will send an email then. + $this->sendLoginAttemptEmail($result, $email); + // Login failure attempt written to log + $this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, SystemLogErrorClassification::MESSAGE, 3, 'Failure warning (%s failures within %s seconds) sent by email to %s', [$rowCount, $secondsBack, $email]); + } + } + + /** + * Sends out an email if the number of attempts have exceeded a limit. + * + * @param Statement $result + * @param string $emailAddress + */ + protected function sendLoginAttemptEmail(Statement $result, string $emailAddress): void + { + $emailData = []; + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + $theData = unserialize($row['log_data'], ['allowed_classes' => false]); + $text = @sprintf($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]); + if ((int)$row['type'] === SystemLogType::LOGIN) { + $text = str_replace('###IP###', $row['IP'], $text); } + $emailData[] = [ + 'row' => $row, + 'text' => $text + ]; + } + $email = GeneralUtility::makeInstance(FluidEmail::class) + ->to($emailAddress) + ->setTemplate('Security/LoginAttemptFailedWarning') + ->assign('lines', $emailData); + if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) { + $email->setRequest($GLOBALS['TYPO3_REQUEST']); } + GeneralUtility::makeInstance(Mailer::class)->send($email); } /**