├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── hello-brisphp
└── src
├── Pwned.php
└── ServiceProvider.php
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: valorin
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | .idea/
3 | composer.lock
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Stephen Rees-Carter
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pwned Passwords Validator for Laravel
2 |
3 | **Laravel includes an official Pwned Passwords validator via the `Password::uncompromised()` validation rule, so I recommend checking that out instead: https://laravel.com/docs/10.x/validation#validating-passwords**
4 |
5 |
6 |
7 | The Pwned Password validator checks the user's submitted password (in a registration or password change form) with the awesome
8 | [HIBP Pwned Passwords](https://haveibeenpwned.com/Passwords) service to see if it is a known _pwned password_.
9 | If the password has been pwned, it will fail validation, preventing the user from using that password in your app.
10 |
11 | > Pwned Passwords are half a billion real world passwords previously exposed in data breaches. This exposure makes them unsuitable for ongoing use as they're at much greater risk of being used to take over other accounts.
12 |
13 | This uses the _ranged search_ feature of the Pwned Passwords API, which uses [k-anonymity](https://en.wikipedia.org/wiki/K-anonymity)
14 | to significantly reduce the risk of any information leakage when accessing the API.
15 | For most systems this should be more than secure enough, although you should definitely decide for yourself if it's suitable for your app.
16 |
17 | Please make sure to check out the blog post by Troy Hunt, where he explains how the service works:
18 | .
19 |
20 | Troy worked with Cloudflare on this service, and they have an in depth technical analysis on how it works and the security implications:
21 | .
22 |
23 | Ultimately, it's up to you to decide if it's safe for your app or not.
24 |
25 | ## Installation
26 |
27 | Install the package using Composer:
28 |
29 | ```
30 | composer require valorin/pwned-validator
31 | ```
32 |
33 | Laravel's service provider discovery will automatically configure the Pwned service provider for you.
34 |
35 | Add the validation message to your validation lang file:
36 |
37 | For each language add a validation message to `validation.php` like below
38 |
39 | ```
40 | 'pwned' => 'The :attribute is not secure enough',
41 | ```
42 |
43 | ## Using the `pwned` validator
44 |
45 | After installation, the `pwned` validator will be available for use directly in your validation rules.
46 | ```php
47 | 'password' => 'pwned',
48 | ```
49 |
50 | Within the context of a registration form, it would look like this:
51 | ```php
52 | return Validator::make($data, [
53 | 'name' => 'required|string|max:255',
54 | 'email' => 'required|string|email|max:255|unique:users',
55 | 'password' => 'required|string|min:6|pwned|confirmed',
56 | ]);
57 | ```
58 |
59 | ## Using the Rule Object
60 |
61 | Alternatively, you can use the `Valorin\Pwned\Pwned` [Validation Rule Object](https://laravel.com/docs/5.5/validation#using-rule-objects)
62 | instead of the `pwned` alias if you prefer:
63 |
64 | ```php
65 | return Validator::make($data, [
66 | 'name' => 'required|string|max:255',
67 | 'email' => 'required|string|email|max:255|unique:users',
68 | 'password' => ['required', 'string', 'min:6', new \Valorin\Pwned\Pwned, 'confirmed'],
69 | ]);
70 | ```
71 |
72 | ## Validation message
73 |
74 | You will need to assign your own validation message within the `resources/lang/*/validation.php` file(s).
75 | Both the Rule object and the `pwned` validator alias refer to the validation string `validation.pwned`.
76 |
77 | I haven't set a default language string as it is important you get the language right for your intended users.
78 | In some systems a message like `Your password has been pwned! Please use a new one!` is suitable, while in other systems
79 | you'd be better with something a lot longer:
80 |
81 | > Your password is insufficiently secure as it has been found in known password breaches, please choose a new one. [Need help?](#)
82 |
83 | Thanks to [kanalumaddela](https://github.com/valorin/pwned-validator/pull/2), you can use `:min` in the message to indicate the minimum number of times found set on the validator.
84 |
85 | > Your password is insufficiently secure as it has been found at least :min times in known password breaches, please choose a new one.
86 |
87 | ## Limiting by the number of times the password was pwned
88 |
89 | You can also limit rejected passwords to those that have been pwned a minimum number of times.
90 | For example, `password` has been pwned 3,303,003 times, however `P@ssword!` has only been pwned 118 times.
91 | If we wanted to block `password` but not `P@ssword!`, we can specify the minimum number as 150 like this:
92 |
93 | ```php
94 | 'password' => 'required|string|min:6|pwned:150|confirmed',
95 | ```
96 |
97 | or using the Rule object:
98 | ```php
99 | 'password' => ['required', 'string', 'min:6', new \Valorin\Pwned\Pwned(150), 'confirmed'],
100 | ```
101 |
102 | ## FAQs
103 |
104 | Q: How secure is this?
105 | A: Please check the above linked blog posts by Troy Hunt and Cloudflare, as they will answer your question and help you decide if it's safe enough for you.
106 |
107 | Q: Do you do any caching?
108 | A: Yep! Each prefix query is cached for a week, to prevent constant API requests if the same prefix is checked multiple times.
109 |
110 | Q: Where are the tests?
111 | A: To properly test this code, we need to hit the web service. I don't want to automate that, to avoid abusing this fantastic service. Instead, since it is an incredibly simplistic validator, I've opted to manually test it for now.
112 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "valorin/pwned-validator",
3 | "description": "Super simple Laravel Validator for checking password via the Pwned Passwords service of Have I Been Pwned",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Stephen Rees-Carter",
9 | "email": "stephen@rees-carter.net"
10 | }
11 | ],
12 | "autoload": {
13 | "psr-4": {
14 | "Valorin\\Pwned\\": "src/"
15 | }
16 | },
17 | "require": {
18 | "php": ">=7.2",
19 | "ext-curl": "*",
20 | "illuminate/support": "^5.5||^6.0||^7.0||^8.0|^9.0|^10.0||^11.0||^12.0"
21 | },
22 | "extra": {
23 | "laravel": {
24 | "providers": [
25 | "Valorin\\Pwned\\ServiceProvider"
26 | ]
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/hello-brisphp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/valorin/pwned-validator/85327224a0816a842483a15330411a634c3ebdb7/hello-brisphp
--------------------------------------------------------------------------------
/src/Pwned.php:
--------------------------------------------------------------------------------
1 | minimum = $minimum;
21 | }
22 |
23 | public function validate($attribute, $value, $params)
24 | {
25 | $this->minimum = array_shift($params) ?? 1;
26 |
27 | return $this->passes($attribute, $value);
28 | }
29 |
30 | public function passes($attribute, $value)
31 | {
32 | list($prefix, $suffix) = $this->hashAndSplit($value);
33 | $results = $this->query($prefix);
34 | $count = $results[$suffix] ?? 0;
35 |
36 | return $count < $this->minimum;
37 | }
38 |
39 | public function message()
40 | {
41 | return Lang::get('validation.pwned');
42 | }
43 |
44 | private function hashAndSplit($value)
45 | {
46 | $hash = strtoupper(sha1($value));
47 | $prefix = substr($hash, 0, 5);
48 | $suffix = substr($hash, 5);
49 |
50 | return [$prefix, $suffix];
51 | }
52 |
53 | private function query($prefix)
54 | {
55 | // Cache results for a week, to avoid constant API calls for identical prefixes
56 | return Cache::remember('pwned:'.$prefix, Carbon::now()->addWeek(), function () use ($prefix) {
57 | $curl = curl_init('https://api.pwnedpasswords.com/range/'.$prefix);
58 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
59 | //Set Add-Padding to true to pad queries. (See: https://haveibeenpwned.com/API/v3#PwnedPasswordsPadding)
60 | curl_setopt($curl, CURLOPT_HTTPHEADER, ['Add-Padding: true']);
61 | $results = curl_exec($curl);
62 | curl_close($curl);
63 |
64 | $hashes = explode("\n", trim($results));
65 |
66 | return (new Collection($hashes))
67 | ->mapWithKeys(function ($value) {
68 | $pair = explode(':', trim($value), 2);
69 |
70 | return count($pair) === 2 && is_numeric($pair[1])
71 | ? [$pair[0] => $pair[1]]
72 | : [];
73 | });
74 | });
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 |