diff --git a/composer.json b/composer.json index ae14b7ea836..f5284c53d4f 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "ocramius/proxy-manager": "^1.0|^2.0", "doctrine/doctrine-bundle": "~1.3", "liip/imagine-bundle": "~1.0", + "ircmaxell/password-compat": "^1.0", "oneup/flysystem-bundle": "^1.0", "friendsofsymfony/http-cache-bundle": "~1.2|^1.3.8", "sensio/framework-extra-bundle": "~3.0", diff --git a/data/cleandata.sql b/data/cleandata.sql index a13df48de72..11646a252aa 100644 --- a/data/cleandata.sql +++ b/data/cleandata.sql @@ -1970,8 +1970,8 @@ INSERT INTO `ezurlalias_ml_incr` (`id`) VALUES (35); INSERT INTO `ezurlalias_ml_incr` (`id`) VALUES (36); INSERT INTO `ezurlalias_ml_incr` (`id`) VALUES (37); -INSERT INTO `ezuser` (`contentobject_id`, `email`, `login`, `password_hash`, `password_hash_type`) VALUES (10,'nospam@ez.no','anonymous','4e6f6184135228ccd45f8233d72a0363',2); -INSERT INTO `ezuser` (`contentobject_id`, `email`, `login`, `password_hash`, `password_hash_type`) VALUES (14,'nospam@ez.no','admin','c78e3b0f3d9244ed8c6d1c29464bdff9',2); +INSERT INTO `ezuser` (`contentobject_id`, `email`, `login`, `password_hash`, `password_hash_type`) VALUES (10,'nospam@ez.no','anonymous','$2y$10$35gOSQs6JK4u4whyERaeUuVeQBi2TUBIZIfP7HEj7sfz.MxvTuOeC',7); +INSERT INTO `ezuser` (`contentobject_id`, `email`, `login`, `password_hash`, `password_hash_type`) VALUES (14,'nospam@ez.no','admin','$2y$10$FDn9NPwzhq85cLLxfD5Wu.L3SL3Z/LNCvhkltJUV0wcJj7ciJg2oy',7); INSERT INTO `ezuser_role` (`contentobject_id`, `id`, `limit_identifier`, `limit_value`, `role_id`) VALUES (11,28,'','',1); INSERT INTO `ezuser_role` (`contentobject_id`, `id`, `limit_identifier`, `limit_value`, `role_id`) VALUES (42,31,'','',1); diff --git a/data/mysql/schema.sql b/data/mysql/schema.sql index 6e27bff65a6..311522cd7e0 100644 --- a/data/mysql/schema.sql +++ b/data/mysql/schema.sql @@ -2222,7 +2222,7 @@ CREATE TABLE `ezuser` ( `contentobject_id` int(11) NOT NULL DEFAULT '0', `email` varchar(150) NOT NULL DEFAULT '', `login` varchar(150) NOT NULL DEFAULT '', - `password_hash` varchar(50) DEFAULT NULL, + `password_hash` varchar(255) DEFAULT NULL, `password_hash_type` int(11) NOT NULL DEFAULT '1', PRIMARY KEY (`contentobject_id`), UNIQUE KEY `ezuser_login` (`login`) diff --git a/doc/bc/changes-6.12.md b/doc/bc/changes-6.12.md new file mode 100644 index 00000000000..85d9eb5896b --- /dev/null +++ b/doc/bc/changes-6.12.md @@ -0,0 +1,18 @@ +# Backwards compatibility changes + +Changes affecting version compatibility with former or future versions. + +## Changes + +- Increase password security + EZP-24744 - Increase password security introduced two new user password hash types, `PASSWORD_HASH_BCRYPT` and + `PASSWORD_HASH_PHP_DEFAULT`. Either one allows the password to be stored with encryption instead of the previous + default, which uses MD5 hashing. `PASSWORD_HASH_BCRYPT` uses Blowfish (bcrypt). `PASSWORD_HASH_PHP_DEFAULT` is + the new default setting, this uses whichever method PHP considers appropriate. Currently this is bcrypt, but + this may change over time. + Caution - Using either of these new types requires that the length of the `password_hash` column of the `ezuser` + database table is increased from the current value of 50 to 255, see the updated database schema. + +## Deprecations + +## Removed features diff --git a/eZ/Publish/API/Repository/Tests/FieldType/UserIntegrationTest.php b/eZ/Publish/API/Repository/Tests/FieldType/UserIntegrationTest.php index 516063f538e..f74ceeb8225 100644 --- a/eZ/Publish/API/Repository/Tests/FieldType/UserIntegrationTest.php +++ b/eZ/Publish/API/Repository/Tests/FieldType/UserIntegrationTest.php @@ -9,6 +9,7 @@ namespace eZ\Publish\API\Repository\Tests\FieldType; use eZ\Publish\Core\FieldType\User\Value as UserValue; +use eZ\Publish\Core\Repository\Values\User\User; use eZ\Publish\API\Repository\Values\Content\Field; /** @@ -132,8 +133,7 @@ public function assertFieldDataLoadedCorrect(Field $field) 'hasStoredLogin' => true, 'login' => 'hans', 'email' => 'hans@example.com', - 'passwordHash' => '680869a9873105e365d39a6d14e68e46', - 'passwordHashType' => 2, + 'passwordHashType' => User::PASSWORD_HASH_PHP_DEFAULT, 'enabled' => true, ); diff --git a/eZ/Publish/API/Repository/Tests/UserServiceTest.php b/eZ/Publish/API/Repository/Tests/UserServiceTest.php index aa6bddb557e..e144e5adece 100644 --- a/eZ/Publish/API/Repository/Tests/UserServiceTest.php +++ b/eZ/Publish/API/Repository/Tests/UserServiceTest.php @@ -955,17 +955,11 @@ public function testCreateUserSetsExpectedProperties(User $user) array( 'login' => 'user', 'email' => 'user@example.com', - 'passwordHash' => $this->createHash( - 'user', - 'secret', - $user->hashAlgorithm - ), 'mainLanguageCode' => 'eng-US', ), array( 'login' => $user->login, 'email' => $user->email, - 'passwordHash' => $user->passwordHash, 'mainLanguageCode' => $user->contentInfo->mainLanguageCode, ) ); @@ -1593,18 +1587,12 @@ public function testUpdateUserUpdatesExpectedProperties(User $user) array( 'login' => 'user', 'email' => 'user@example.com', - 'passwordHash' => $this->createHash( - 'user', - 'my-new-password', - $user->hashAlgorithm - ), 'maxLogin' => 42, 'enabled' => false, ), array( 'login' => $user->login, 'email' => $user->email, - 'passwordHash' => $user->passwordHash, 'maxLogin' => $user->maxLogin, 'enabled' => $user->enabled, ) @@ -2490,25 +2478,4 @@ private function createMultiLanguageUser($userGroupId = 13) // Create a new user return $userService->createUser($userCreateStruct, [$group]); } - - private function createHash($login, $password, $type) - { - switch ($type) { - case 2: - /* PASSWORD_HASH_MD5_USER */ - return md5("{$login}\n{$password}"); - - case 3: - /* PASSWORD_HASH_MD5_SITE */ - $site = null; - - return md5("{$login}\n{$password}\n{$site}"); - - case 5: - /* PASSWORD_HASH_PLAINTEXT */ - return $password; - } - /* PASSWORD_HASH_MD5_PASSWORD (1) */ - return md5($password); - } } diff --git a/eZ/Publish/Core/FieldType/Tests/Integration/User/UserStorage/UserStorageGatewayTest.php b/eZ/Publish/Core/FieldType/Tests/Integration/User/UserStorage/UserStorageGatewayTest.php index 26189be1468..ee4837e603a 100644 --- a/eZ/Publish/Core/FieldType/Tests/Integration/User/UserStorage/UserStorageGatewayTest.php +++ b/eZ/Publish/Core/FieldType/Tests/Integration/User/UserStorage/UserStorageGatewayTest.php @@ -9,6 +9,7 @@ namespace eZ\Publish\Core\FieldType\Tests\Integration\User\UserStorage; use eZ\Publish\Core\FieldType\Tests\Integration\BaseCoreFieldTypeIntegrationTest; +use eZ\Publish\Core\Repository\Values\User\User; /** * User Field Type external storage gateway tests. @@ -31,8 +32,8 @@ public function providerForGetFieldData() 'contentId' => 10, 'login' => 'anonymous', 'email' => 'nospam@ez.no', - 'passwordHash' => '4e6f6184135228ccd45f8233d72a0363', - 'passwordHashType' => '2', + 'passwordHash' => '$2y$10$35gOSQs6JK4u4whyERaeUuVeQBi2TUBIZIfP7HEj7sfz.MxvTuOeC', + 'passwordHashType' => User::PASSWORD_HASH_PHP_DEFAULT, 'enabled' => true, 'maxLogin' => 1000, ], @@ -41,8 +42,8 @@ public function providerForGetFieldData() 'contentId' => 14, 'login' => 'admin', 'email' => 'spam@ez.no', - 'passwordHash' => 'c78e3b0f3d9244ed8c6d1c29464bdff9', - 'passwordHashType' => '2', + 'passwordHash' => '$2y$10$FDn9NPwzhq85cLLxfD5Wu.L3SL3Z/LNCvhkltJUV0wcJj7ciJg2oy', + 'passwordHashType' => User::PASSWORD_HASH_PHP_DEFAULT, 'enabled' => true, 'maxLogin' => 10, ], diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.mysql.sql b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.mysql.sql index 3b7fd2be23f..f0c7b74c366 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.mysql.sql +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.mysql.sql @@ -482,7 +482,7 @@ CREATE TABLE ezuser ( contentobject_id int(11) NOT NULL DEFAULT 0, email varchar(150) NOT NULL DEFAULT '', login varchar(150) NOT NULL DEFAULT '', - password_hash varchar(50) DEFAULT NULL, + password_hash varchar(255) DEFAULT NULL, password_hash_type int(11) NOT NULL DEFAULT 1, PRIMARY KEY (contentobject_id), UNIQUE KEY `ezuser_login` (`login`) diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.pgsql.sql b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.pgsql.sql index 4a8d5f5acc9..0a2740e09dd 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.pgsql.sql +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.pgsql.sql @@ -512,7 +512,7 @@ CREATE TABLE ezuser ( contentobject_id integer DEFAULT 0 NOT NULL, email character varying(150) DEFAULT ''::character varying NOT NULL, login character varying(150) DEFAULT ''::character varying NOT NULL, - password_hash character varying(50), + password_hash character varying(255), password_hash_type integer DEFAULT 1 NOT NULL ); diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.sqlite.sql b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.sqlite.sql index f0c906e5188..c206526f06d 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.sqlite.sql +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/_fixtures/schema.sqlite.sql @@ -432,7 +432,7 @@ CREATE TABLE ezuser ( contentobject_id integer NOT NULL DEFAULT 0, email text(150) NOT NULL, login text(150) NOT NULL, - password_hash text(50), + password_hash text(255), password_hash_type integer NOT NULL DEFAULT 1, PRIMARY KEY (contentobject_id) ); diff --git a/eZ/Publish/Core/Persistence/Legacy/User/Mapper.php b/eZ/Publish/Core/Persistence/Legacy/User/Mapper.php index 12ffd18660c..4670b7446ac 100644 --- a/eZ/Publish/Core/Persistence/Legacy/User/Mapper.php +++ b/eZ/Publish/Core/Persistence/Legacy/User/Mapper.php @@ -33,7 +33,7 @@ public function mapUser(array $data) $user->login = $data['login']; $user->email = $data['email']; $user->passwordHash = $data['password_hash']; - $user->hashAlgorithm = $data['password_hash_type']; + $user->hashAlgorithm = (int)$data['password_hash_type']; $user->isEnabled = (bool)$data['is_enabled']; $user->maxLogin = $data['max_login']; diff --git a/eZ/Publish/Core/Repository/Tests/Service/Integration/Legacy/_fixtures/clean_ezdemo_47_dump.php b/eZ/Publish/Core/Repository/Tests/Service/Integration/Legacy/_fixtures/clean_ezdemo_47_dump.php index 2174b740b5f..4c07de6b459 100644 --- a/eZ/Publish/Core/Repository/Tests/Service/Integration/Legacy/_fixtures/clean_ezdemo_47_dump.php +++ b/eZ/Publish/Core/Repository/Tests/Service/Integration/Legacy/_fixtures/clean_ezdemo_47_dump.php @@ -11469,15 +11469,15 @@ 'contentobject_id' => '10', 'email' => 'nospam@ez.no', 'login' => 'anonymous', - 'password_hash' => '4e6f6184135228ccd45f8233d72a0363', - 'password_hash_type' => '2', + 'password_hash' => '$2y$10$35gOSQs6JK4u4whyERaeUuVeQBi2TUBIZIfP7HEj7sfz.MxvTuOeC', + 'password_hash_type' => '7', ), 1 => array( 'contentobject_id' => '14', 'email' => 'spam@ez.no', 'login' => 'admin', - 'password_hash' => 'c78e3b0f3d9244ed8c6d1c29464bdff9', - 'password_hash_type' => '2', + 'password_hash' => '$2y$10$FDn9NPwzhq85cLLxfD5Wu.L3SL3Z/LNCvhkltJUV0wcJj7ciJg2oy', + 'password_hash_type' => '7', ), ), 'ezuser_accountkey' => array( diff --git a/eZ/Publish/Core/Repository/Tests/Service/Integration/Legacy/_fixtures/clean_ezflow_dump.php b/eZ/Publish/Core/Repository/Tests/Service/Integration/Legacy/_fixtures/clean_ezflow_dump.php index a5abffa9c3c..b890587d3f8 100644 --- a/eZ/Publish/Core/Repository/Tests/Service/Integration/Legacy/_fixtures/clean_ezflow_dump.php +++ b/eZ/Publish/Core/Repository/Tests/Service/Integration/Legacy/_fixtures/clean_ezflow_dump.php @@ -13089,15 +13089,15 @@ 'contentobject_id' => '10', 'email' => 'nospam@ez.no', 'login' => 'anonymous', - 'password_hash' => '4e6f6184135228ccd45f8233d72a0363', - 'password_hash_type' => '2', + 'password_hash' => '$2y$10$35gOSQs6JK4u4whyERaeUuVeQBi2TUBIZIfP7HEj7sfz.MxvTuOeC', + 'password_hash_type' => '7', ), 1 => array( 'contentobject_id' => '14', 'email' => 'spam@ez.no', 'login' => 'admin', - 'password_hash' => 'c78e3b0f3d9244ed8c6d1c29464bdff9', - 'password_hash_type' => '2', + 'password_hash' => '$2y$10$FDn9NPwzhq85cLLxfD5Wu.L3SL3Z/LNCvhkltJUV0wcJj7ciJg2oy', + 'password_hash_type' => '7', ), ), 'ezuser_accountkey' => array( diff --git a/eZ/Publish/Core/Repository/UserService.php b/eZ/Publish/Core/Repository/UserService.php index 14c8ebf4cb8..c1e80ebdd66 100644 --- a/eZ/Publish/Core/Repository/UserService.php +++ b/eZ/Publish/Core/Repository/UserService.php @@ -77,7 +77,7 @@ public function __construct(RepositoryInterface $repository, Handler $userHandle 'defaultUserPlacement' => 12, 'userClassID' => 4, // @todo Rename this settings to swap out "Class" for "Type" 'userGroupClassID' => 3, - 'hashType' => User::PASSWORD_HASH_MD5_USER, + 'hashType' => User::PASSWORD_HASH_PHP_DEFAULT, 'siteName' => 'ez.no', ); } @@ -565,18 +565,8 @@ public function loadUserByCredentials($login, $password, array $prioritizedLangu throw new InvalidArgumentValue('password', $password); } - // Randomize login time to protect against timing attacks - usleep(mt_rand(0, 30000)); - $spiUser = $this->userHandler->loadByLogin($login); - $passwordHash = $this->createPasswordHash( - $login, - $password, - $this->settings['siteName'], - $spiUser->hashAlgorithm - ); - - if ($spiUser->passwordHash !== $passwordHash) { + if (!$this->verifyPassword($login, $password, $spiUser)) { throw new NotFoundException('user', $login); } @@ -1120,6 +1110,37 @@ protected function buildDomainUserObject( ); } + /** + * Verifies if the provided login and password are valid. + * + * @param string $login User login + * @param string $password User password + * @param \eZ\Publish\SPI\Persistence\User $spiUser Loaded user handler + * + * @return bool return true if the login and password are sucessfully + * validate and false, if not. + */ + protected function verifyPassword($login, $password, $spiUser) + { + // In case of bcrypt let php's password functionality do it's magic + if ($spiUser->hashAlgorithm === User::PASSWORD_HASH_BCRYPT || + $spiUser->hashAlgorithm === User::PASSWORD_HASH_PHP_DEFAULT) { + return password_verify($password, $spiUser->passwordHash); + } + + // Randomize login time to protect against timing attacks + usleep(mt_rand(0, 30000)); + + $passwordHash = $this->createPasswordHash( + $login, + $password, + $this->settings['siteName'], + $spiUser->hashAlgorithm + ); + + return $passwordHash === $spiUser->passwordHash; + } + /** * Returns password hash based on user data and site settings. * @@ -1145,6 +1166,12 @@ protected function createPasswordHash($login, $password, $site, $type) case User::PASSWORD_HASH_PLAINTEXT: return $password; + case User::PASSWORD_HASH_BCRYPT: + return password_hash($password, PASSWORD_BCRYPT); + + case User::PASSWORD_HASH_PHP_DEFAULT: + return password_hash($password, PASSWORD_DEFAULT); + default: return md5($password); } diff --git a/eZ/Publish/Core/Repository/Values/User/User.php b/eZ/Publish/Core/Repository/Values/User/User.php index 662ac2f4cf9..ebab3d3d419 100644 --- a/eZ/Publish/Core/Repository/Values/User/User.php +++ b/eZ/Publish/Core/Repository/Values/User/User.php @@ -37,6 +37,16 @@ class User extends APIUser */ const PASSWORD_HASH_PLAINTEXT = 5; + /** + * @var int Passwords in bcrypt + */ + const PASSWORD_HASH_BCRYPT = 6; + + /** + * @var int Passwords hashed by PHPs default algorithm, which may change over time + */ + const PASSWORD_HASH_PHP_DEFAULT = 7; + /** * Internal content representation. * diff --git a/eZ/Publish/Core/Search/Legacy/Tests/_fixtures/full_dump.php b/eZ/Publish/Core/Search/Legacy/Tests/_fixtures/full_dump.php index b9a9503e60e..6825d8af8b1 100644 --- a/eZ/Publish/Core/Search/Legacy/Tests/_fixtures/full_dump.php +++ b/eZ/Publish/Core/Search/Legacy/Tests/_fixtures/full_dump.php @@ -47667,24 +47667,24 @@ 'contentobject_id' => '10', 'email' => 'nospam@ez.no', 'login' => 'anonymous', - 'password_hash' => '4e6f6184135228ccd45f8233d72a0363', - 'password_hash_type' => '2', + 'password_hash' => '$2y$10$35gOSQs6JK4u4whyERaeUuVeQBi2TUBIZIfP7HEj7sfz.MxvTuOeC', + 'password_hash_type' => '7', ), 1 => array ( 'contentobject_id' => '14', 'email' => 'kn@ez.no', 'login' => 'admin', - 'password_hash' => 'c78e3b0f3d9244ed8c6d1c29464bdff9', - 'password_hash_type' => '2', + 'password_hash' => '$2y$10$FDn9NPwzhq85cLLxfD5Wu.L3SL3Z/LNCvhkltJUV0wcJj7ciJg2oy', + 'password_hash_type' => '7', ), 2 => array ( 'contentobject_id' => '226', 'email' => 'pa@ez.no', 'login' => 'a_member', - 'password_hash' => 'c78e3b0f3d9244ed8c6d1c29464bdff9', - 'password_hash_type' => '2', + 'password_hash' => '$2y$10$FDn9NPwzhq85cLLxfD5Wu.L3SL3Z/LNCvhkltJUV0wcJj7ciJg2oy', + 'password_hash_type' => '7', ), ), 'ezuser_role' =>