From 83a6090d55ac3ef8a6f656ec3dc22f0bf88d413e Mon Sep 17 00:00:00 2001 From: Oliver Bartsch <bo@cedev.de> Date: Mon, 7 Nov 2022 23:23:10 +0100 Subject: [PATCH] [FOLLOWUP][BUGFIX] Prevent out of bounds exception in password generation This followup prevents an out of bounds exception and adds a couple of tests for the new functionality. The min length is now set to 8. This is now also checked when using the special "random" option. Resolves: #98540 Releases: main Change-Id: I5e89813794a0178b7d9c835e89c6272128b4b8c3 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76461 Reviewed-by: Benni Mack <benni@typo3.org> Reviewed-by: Georg Ringer <georg.ringer@gmail.com> Reviewed-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Oliver Bartsch <bo@cedev.de> Tested-by: core-ci <typo3@b13.com> Tested-by: Georg Ringer <georg.ringer@gmail.com> Tested-by: Christian Kuhn <lolli@schwarzbu.ch> Tested-by: Oliver Bartsch <bo@cedev.de> --- typo3/sysext/core/Classes/Crypto/Random.php | 21 ++- ...98540-NewFieldControlPasswordGenerator.rst | 2 +- .../core/Tests/Unit/Crypto/RandomTest.php | 147 ++++++++++++++++++ 3 files changed, 162 insertions(+), 8 deletions(-) diff --git a/typo3/sysext/core/Classes/Crypto/Random.php b/typo3/sysext/core/Classes/Crypto/Random.php index 8de171374cde..bd21b55bb283 100644 --- a/typo3/sysext/core/Classes/Crypto/Random.php +++ b/typo3/sysext/core/Classes/Crypto/Random.php @@ -74,14 +74,21 @@ class Random */ public function generateRandomPassword(array $passwordRules): string { - $password = ''; $passwordLength = (int)($passwordRules['length'] ?? self::DEFAULT_PASSWORD_LEGNTH); + if ($passwordLength < 8) { + throw new InvalidPasswordRulesException( + 'Password rules are invalid. Length must be at least 8.', + 1667557900 + ); + } + + $password = ''; if ($passwordRules['random'] ?? false) { $password = match ((string)$passwordRules['random']) { 'hex' => $this->generateRandomHexString($passwordLength), 'base64' => $this->generateRandomBase64String($passwordLength), - default => throw new InvalidPasswordRulesException('Invalid value for special option \'random\'. Valid options are: \'hex\' and \'base64\'', 1667557901), + default => throw new InvalidPasswordRulesException('Invalid value for special password rule \'random\'. Valid options are: \'hex\' and \'base64\'', 1667557901), }; } else { $characters = []; @@ -103,20 +110,20 @@ class Random $characterSets[] = self::SPECIAL_CHARACTERS; } - if ($passwordLength <= 0 || $characterSets === []) { + if ($characterSets === []) { throw new InvalidPasswordRulesException( - 'Password rules are invalid. Length must be grater 0 and at least one character set must be allowed.', - 1667557900 + 'Password rules are invalid. At least one character set must be allowed.', + 1667557902 ); } foreach ($characterSets as $characterSet) { - $password .= $characterSet[random_int(0, strlen($characterSet))]; + $password .= $characterSet[random_int(0, strlen($characterSet) - 1)]; } $charactersCount = count($characters); for ($i = 0; $i < $passwordLength - count($characterSets); $i++) { - $password .= $characters[random_int(0, $charactersCount)]; + $password .= $characters[random_int(0, $charactersCount - 1)]; } str_shuffle($password); diff --git a/typo3/sysext/core/Documentation/Changelog/12.1/Feature-98540-NewFieldControlPasswordGenerator.rst b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-98540-NewFieldControlPasswordGenerator.rst index a494758c4a05..de5d3abcc258 100644 --- a/typo3/sysext/core/Documentation/Changelog/12.1/Feature-98540-NewFieldControlPasswordGenerator.rst +++ b/typo3/sysext/core/Documentation/Changelog/12.1/Feature-98540-NewFieldControlPasswordGenerator.rst @@ -61,7 +61,7 @@ Field control options Available password rules: -- :php:`length`: Defines the amout of characters for the password (Default: :php:`16`). +- :php:`length`: Defines the amout of characters for the password (minimum: :php:`8` - default: :php:`16`). - :php:`random`: Defines the encoding of random bytes. Overrules charater definitions. Available encodings are :php:`hex` and :php:`base64`. - :php:`digitCharacters`: Whether digits should be used (Default: :php:`true`) - :php:`lowerCaseCharacters`: Whether lowercase characters should be used (Default: :php:`true`) diff --git a/typo3/sysext/core/Tests/Unit/Crypto/RandomTest.php b/typo3/sysext/core/Tests/Unit/Crypto/RandomTest.php index abd96b84c649..efa9eef87b4a 100644 --- a/typo3/sysext/core/Tests/Unit/Crypto/RandomTest.php +++ b/typo3/sysext/core/Tests/Unit/Crypto/RandomTest.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Tests\Unit\Crypto; use TYPO3\CMS\Core\Crypto\Random; +use TYPO3\CMS\Core\Exception\InvalidPasswordRulesException; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; class RandomTest extends UnitTestCase @@ -67,4 +68,150 @@ class RandomTest extends UnitTestCase $subject = new Random(); self::assertEquals($numberOfChars, strlen($subject->generateRandomHexString($numberOfChars))); } + + public function generateRandomPasswordThrowsInvalidPasswordRulesExceptionDataProvider(): \Generator + { + yield 'Invalid length' => [ + [ + 'length' => 4, + ], + 1667557900, + ]; + yield 'Invalid random value' => [ + [ + 'random' => 'invalid', + ], + 1667557901, + ]; + yield 'Invalid characters definition' => [ + [ + 'lowerCaseCharacters' => false, + 'upperCaseCharacters' => false, + 'digitCharacters' => false, + ], + 1667557902, + ]; + } + + /** + * @test + * @dataProvider generateRandomPasswordThrowsInvalidPasswordRulesExceptionDataProvider + */ + public function generateRandomPasswordThrowsInvalidPasswordRulesException( + array $passwordRules, + int $exceptionCode + ): void { + $this->expectException(InvalidPasswordRulesException::class); + $this->expectExceptionCode($exceptionCode); + + (new Random())->generateRandomPassword($passwordRules); + } + + public function generateRandomPasswordGeneratesRandomWithEncodingDataProvider(): \Generator + { + yield 'Hex with 42 chars' => [ + [ + 'length' => 42, + 'random' => 'hex', + ], + '/^[a-fA-F0-9]{42}$/', + ]; + yield 'Base64 with 37 chars' => [ + [ + 'length' => 37, + 'random' => 'base64', + 'digitCharacters' => false, // Won't be evaluated + ], + '/^[a-zA-Z0-9\-\_]{37}$/', + ]; + } + + /** + * @test + * @dataProvider generateRandomPasswordGeneratesRandomWithEncodingDataProvider + */ + public function generateRandomPasswordGeneratesRandomWithEncoding( + array $passwordRules, + string $pattern + ): void { + self::assertMatchesRegularExpression($pattern, (new Random())->generateRandomPassword($passwordRules)); + } + + public function generateRandomPasswordGeneratesRandomWithCharacterSetsDataProvider(): \Generator + { + yield 'lowercase' => [ + [ + 'lowerCaseCharacters' => true, + ], + '/[a-z]+/', + ]; + yield 'uppercase' => [ + [ + 'upperCaseCharacters' => true, + ], + '/[A-Z]+/', + ]; + yield 'digits' => [ + [ + 'digitCharacters' => true, + ], + '/[0-9]+/', + ]; + yield 'special' => [ + [ + 'specialCharacters' => true, + ], + '/[\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]+/', + ]; + } + + /** + * @test + * @dataProvider generateRandomPasswordGeneratesRandomWithCharacterSetsDataProvider + */ + public function generateRandomPasswordGeneratesRandomWithCharacterSets( + array $passwordRules, + string $pattern + ): void { + self::assertMatchesRegularExpression($pattern, (new Random())->generateRandomPassword($passwordRules)); + } + + public function generateRandomPasswordGeneratesRandomWithLengthDataProvider(): \Generator + { + yield 'fallback' => [ + [], + 16, + ]; + yield 'length=40' => [ + [ + 'length' => 40, + ], + 40, + ]; + yield 'length=36 with random=hex' => [ + [ + 'length' => 36, + 'random' => 'hex', + ], + 36, + ]; + yield 'length=42 with random=hex' => [ + [ + 'length' => 42, + 'random' => 'base64', + ], + 42, + ]; + } + + /** + * @test + * @dataProvider generateRandomPasswordGeneratesRandomWithLengthDataProvider + */ + public function generateRandomPasswordGeneratesRandomWithLength( + array $passwordRules, + int $length + ): void { + self::assertEquals($length, strlen((new Random())->generateRandomPassword($passwordRules))); + } } -- GitLab