[make:user] Hash passwords using crc32c and deprecate eraseCredentials()

This commit is contained in:
Nicolas Grekas
2025-05-13 14:47:22 +02:00
parent 468ff27082
commit 75886fbd3e
11 changed files with 165 additions and 27 deletions

View File

@@ -33,6 +33,7 @@
"symfony/http-client": "^6.4|^7.0",
"symfony/phpunit-bridge": "^6.4.1|^7.0",
"symfony/security-core": "^6.4|^7.0",
"symfony/security-http": "^6.4|^7.0",
"symfony/yaml": "^6.4|^7.0",
"twig/twig": "^3.0|^4.x-dev"
},

View File

@@ -17,6 +17,7 @@ use Symfony\Bundle\MakerBundle\Util\ClassSource\Model\ClassProperty;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Attribute\IsGrantedContext;
/**
* Adds logic to implement UserInterface to an existing User class.
@@ -37,7 +38,13 @@ final class UserClassBuilder
$this->addPasswordImplementation($manipulator, $userClassConfig);
$this->addEraseCredentials($manipulator);
if (class_exists(IsGrantedContext::class)) {
$this->addSerialize($manipulator);
}
if (method_exists(UserInterface::class, 'eraseCredentials')) {
$this->addEraseCredentials($manipulator);
}
}
private function addPasswordImplementation(ClassSourceManipulator $manipulator, UserClassConfiguration $userClassConfig): void
@@ -245,17 +252,80 @@ final class UserClassBuilder
$builder = $manipulator->createMethodBuilder(
'eraseCredentials',
'void',
false,
['@see UserInterface']
false
);
$builder->addAttribute(new Node\Attribute(new Node\Name('\Deprecated')));
$builder->addStmt(
$manipulator->createMethodLevelCommentNode(
'If you store any temporary, sensitive data on the user, clear it here'
'@deprecated, to be removed when upgrading to Symfony 8'
)
);
$manipulator->addMethodBuilder($builder);
}
private function addSerialize(ClassSourceManipulator $manipulator): void
{
$builder = $manipulator->createMethodBuilder(
'__serialize',
'array',
false,
[
'Ensure the session doesn\'t contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.',
]
);
// $data = (array) $this;
$builder->addStmt(
$manipulator->createMethodLevelCommentNode(
'$this->plainPassword = null;'
new Node\Stmt\Expression(
new Node\Expr\Assign(
new Node\Expr\Variable('data'),
new Node\Expr\Cast\Array_(
new Node\Expr\Variable('this')
)
)
)
);
// $data["\0".self::class."\0password"] = hash('crc32c', $this->password);
$builder->addStmt(
new Node\Stmt\Expression(
new Node\Expr\Assign(
new Node\Expr\ArrayDimFetch(
new Node\Expr\Variable('data'),
new Node\Expr\BinaryOp\Concat(
new Node\Expr\BinaryOp\Concat(
new Node\Scalar\String_("\0", ['kind' => Node\Scalar\String_::KIND_DOUBLE_QUOTED]),
new Node\Expr\ClassConstFetch(
new Node\Name('self'),
'class'
)
),
new Node\Scalar\String_("\0password", ['kind' => Node\Scalar\String_::KIND_DOUBLE_QUOTED]),
)
),
new Node\Expr\FuncCall(
new Node\Name('hash'),
[
new Node\Arg(new Node\Scalar\String_('crc32c')),
new Node\Arg(
new Node\Expr\PropertyFetch(
new Node\Expr\Variable('this'),
'password'
)
),
]
)
)
)
);
$builder->addStmt(new Node\Stmt\Nop());
// return $data;
$builder->addStmt(
new Node\Stmt\Return_(
new Node\Expr\Variable('data')
)
);

View File

@@ -59,6 +59,7 @@ final class PrettyPrinter extends Standard
if ($node->returnType) {
$classMethod = str_replace(') :', '):', $classMethod);
}
$classMethod = str_replace('\x00', '\0', $classMethod);
return $classMethod;
}

View File

@@ -15,6 +15,8 @@ use PHPUnit\Framework\TestCase;
use Symfony\Bundle\MakerBundle\Security\UserClassBuilder;
use Symfony\Bundle\MakerBundle\Security\UserClassConfiguration;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Attribute\IsGrantedContext;
class UserClassBuilderTest extends TestCase
{
@@ -31,6 +33,14 @@ class UserClassBuilderTest extends TestCase
$expectedPath = $this->getExpectedPath($expectedFilename, null);
$expectedSource = file_get_contents($expectedPath);
if (!class_exists(IsGrantedContext::class)) {
$expectedSource = preg_replace('/\n\n(.+\n)+.+function __serialize[^}]++}/', '', $expectedSource);
}
if (!method_exists(UserInterface::class, 'eraseCredentials')) {
$expectedSource = preg_replace('/\n\n(.+\n)+.+function eraseCredentials[^}]++}/', '', $expectedSource);
}
self::assertSame($expectedSource, $manipulator->getSourceCode());
}

View File

@@ -96,11 +96,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
}
/**
* @see UserInterface
* Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
*/
public function __serialize(): array
{
$data = (array) $this;
$data["\0" . self::class . "\0password"] = hash('crc32c', $this->password);
return $data;
}
#[\Deprecated]
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
// @deprecated, to be removed when upgrading to Symfony 8
}
}

View File

@@ -91,11 +91,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
}
/**
* @see UserInterface
* Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
*/
public function __serialize(): array
{
$data = (array) $this;
$data["\0" . self::class . "\0password"] = hash('crc32c', $this->password);
return $data;
}
#[\Deprecated]
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
// @deprecated, to be removed when upgrading to Symfony 8
}
}

View File

@@ -91,11 +91,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
}
/**
* @see UserInterface
* Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
*/
public function __serialize(): array
{
$data = (array) $this;
$data["\0" . self::class . "\0password"] = hash('crc32c', $this->password);
return $data;
}
#[\Deprecated]
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
// @deprecated, to be removed when upgrading to Symfony 8
}
}

View File

@@ -69,11 +69,19 @@ class User implements UserInterface
}
/**
* @see UserInterface
* Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
*/
public function __serialize(): array
{
$data = (array) $this;
$data["\0" . self::class . "\0password"] = hash('crc32c', $this->password);
return $data;
}
#[\Deprecated]
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
// @deprecated, to be removed when upgrading to Symfony 8
}
}

View File

@@ -80,11 +80,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
}
/**
* @see UserInterface
* Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
*/
public function __serialize(): array
{
$data = (array) $this;
$data["\0" . self::class . "\0password"] = hash('crc32c', $this->password);
return $data;
}
#[\Deprecated]
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
// @deprecated, to be removed when upgrading to Symfony 8
}
}

View File

@@ -75,11 +75,19 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
}
/**
* @see UserInterface
* Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
*/
public function __serialize(): array
{
$data = (array) $this;
$data["\0" . self::class . "\0password"] = hash('crc32c', $this->password);
return $data;
}
#[\Deprecated]
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
// @deprecated, to be removed when upgrading to Symfony 8
}
}

View File

@@ -54,11 +54,19 @@ class User implements UserInterface
}
/**
* @see UserInterface
* Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3.
*/
public function __serialize(): array
{
$data = (array) $this;
$data["\0" . self::class . "\0password"] = hash('crc32c', $this->password);
return $data;
}
#[\Deprecated]
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
// @deprecated, to be removed when upgrading to Symfony 8
}
}