New in Symfony 4.3: Compromised password validator

Contributed by
Kévin Dunglas
in #27738.

A data breach is the intentional or unintentional release of secure or private/confidential information to an untrusted environment. The list of data breaches increases every day and, just in the first half of 2018, about 4.5 billion records were exposed, including user passwords.

Users that set their password to any of the publicly exposed passwords are a serious security problem for web sites and applications. That's why services like have i been pwned? allow you to check if your password is compromised.

In Symfony 4.3, we've added a new NotCompromisedPassword constraint to validate that the given password hasn't been compromised:

  • Annotations
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // src/Entity/User.php
    namespace App\Entity;
    
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        // ...
    
        /**
         * @Assert\NotCompromisedPassword
         */
        protected $rawPassword;
    }
    
  • YAML
    1
    2
    3
    4
    5
    # config/validator/validation.yaml
    App\Entity\User:
        properties:
            rawPassword:
                - NotCompromisedPassword
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <!-- config/validator/validation.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">
    
        <class name="App\Entity\User">
            <property name="rawPassword">
                <constraint name="NotCompromisedPassword"></constraint>
            </property>
        </class>
    </constraint-mapping>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // src/Entity/User.php
    namespace App\Entity;
    
    use Symfony\Component\Validator\Mapping\ClassMetadata;
    use Symfony\Component\Validator\Constraints as Assert;
    
    class User
    {
        public static function loadValidatorMetadata(ClassMetadata $metadata)
        {
            $metadata->addPropertyConstraint('rawPassword', new Assert\NotCompromisedPassword());
        }
    }
    

Internally, the constraint makes an HTTP request to the API provided by the haveibeenpwned.com website. In the request, the validator doesn't send the raw password but only the few first characters of the result of encoding it using SHA-1.

For example, if the raw password is test, the SHA-1 hash is a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 and the validator only sends a94a8 to haveibeenpwned.com (the first five characters of the SHA-1 hash). This is called "k-anonymity password validation" and is fully explained in this blog post by Cloudflare.

The HTTP request is made with the new HttpClient component added in Symfony 4.3 and which will we introduced soon in a dedicated blog post.

New in Symfony 4.3: Compromised password validator symfony.com/blog/new-in-symfony-4-3-compromised-password-validator

Tweet this

Comments

Nice, but from other hand isn't this bit risky to ask remote service about your password to build up their brute force DB? :D
So the NotPwned passes the raw password to haveibeenpwned.com API. I am not sure this is something the backend should handle. I think should really happen on the client side.

Also the instead of the database, I think the network/proxy of the servers can be compromised equally and fake CA and Domain and raw password can be exposed.

I am not sure I understand.
Hi @Piotr and @Ersin,

look at the merged pull-request (https://github.com/symfony/symfony/pull/27738).

You will see, that the raw rassword is hashed using sha1 and only the first few characters of that hash to the service (https://blog.cloudflare.com/validating-leaked-passwords-with-k-anonymity/).

So there will be no filling their DB with your passwords :D
The implementation does not send the password to haveibeenpowned, it sends a hash of the 5 first characters, and then compares the password to a list returned by the API ("k-anonymity model"). So it is safe :)
That's a great addition!
@Oliver +1, right I didn't check the PR before commenting. Thx for heads up :)
Probably it would be nice to disable it on dev environment. My fixtures would be crazy right now :)
Thank you all for your comments. I've just updated the blog post with more technical details about how does the password validation work.
@Romaric
> The implementation does not send the password to haveibeenpowned, it sends a hash of the 5 first characters

No. It sends *5 first characters of the password's SHA-1 hash*, not hash of password's first 5 characters.
In my opinion the validator name "NotPwned" is very developer-unfriendly. People who read through my code and don't know that service are pretty much confused what it does.

Why not give it a more speaking name like NotCompromised or NotListedOnHaveibeenpwned?

What do others think about this?
@Piotr Karszny you validate your fixtures?

@Thomas Schulz I agree, maybe it should have been @NotBreached or @NotCompromised.
I think it's a great idea to provide a validation component for passwords into the framework using this functionality. I just wonder why make it a specific Symfony component as many frameworks out there have validation components.

I provided a Composer package "dragonbe/hibp" for the purpose making password validation available to everyone independent from the framework. And to prevent the rebuilding of logic, wouldn't it be better if we have one common base component with each framework implementing that base component? For details, see https://packagist.org/packages/dragonbe/hibp and yes, I accept PR's to make it better suited for all the frameworks.
Nice job. Also may be some user doesnt understand that they cannot choose her own pw.
Good option! what I do not understand, is that if you have put this, because yours have removed or deprecated checkMX, I suppose that in the end the same thing will happen.

Thanks so much!
@Michelangelo van Dam there's nothing "Symfony specific" here. This is shipped in a standalone component, exactly like yours, except that this is backed by all standard maintainance processes of the project.
@Nicolas Grekas Gotcha! Thank you for clarifying.
Computerphile on youtube also has a good video on the topic:
https://www.youtube.com/watch?v=hhUb5iknVJs
@Thomas Schulz You can simply extend the constraint and the validator if that bothers You that much.
Login with SymfonyConnect to post a comment