Laravel’s Password Leak Validator

under

Scrolling through r/laravel last week, I stumbled across a post that stopped me mid-scroll — TIL there’s a built-in validation rule in Laravel to check if the user password has been leaked before — and honestly, it’s one of those moments where you feel both impressed and slightly embarrassed you didn’t know sooner.

Built-in Validation Rule in Laravel to Check Password Leaks

Laravel has quietly shipped one of the most security-conscious validation rules I’ve seen in any framework: Password::uncompromised(). It checks a user’s password against the Have I Been Pwned database using a clever k-anonymity model, meaning it never sends the actual password over the wire. If the password appears in known data breaches, validation fails before it ever touches your database.

This landed in Laravel 8.x as part of the fluent Password rule object, and it’s seen continued improvement through Laravel 10 and 11. The fact that it ships in the framework itself — no package, no extra dependency — is what genuinely got me. Most frameworks make you hunt for a third-party package to do this. Laravel just… includes it.

Here’s the minimal setup:

use Illuminate\Validation\Rules\Password;

$request->validate([
    'password' => ['required', Password::min(8)->uncompromised()],
]);

That’s it. One fluent method call, and you’re checking against hundreds of millions of compromised credentials.

How the k-Anonymity Model Works Under the Hood

Laravel doesn’t send your user’s password to a third-party API in plaintext. Instead it uses a k-anonymity approach:

  1. SHA-1 hash the password locally
  2. Send only the first 5 characters of that hash to the HIBP API
  3. Receive a list of all hashes that share that prefix
  4. Check locally whether the full hash exists in the returned list

The actual implementation lives in Illuminate\Validation\Rules\Password. Laravel makes an HTTP request to https://api.pwnedpasswords.com/range/{hash_prefix} and compares results client-side. Your user’s real password never leaves your server in any identifiable form.

The Full Power of the Fluent Password Rule

The uncompromised() method is just one piece of a broader fluent API that lets you compose password validation rules like building blocks. Most developers reach for min() and stop there. That’s leaving a lot on the table.

Password::min(12)
    ->letters()
    ->mixedCase()
    ->numbers()
    ->symbols()
    ->uncompromised()

Each method in that chain enforces a distinct constraint:

Method What it enforces
min(n) Minimum character length
letters() At least one alphabetic character
mixedCase() At least one uppercase and one lowercase
numbers() At least one numeric character
symbols() At least one special character
uncompromised(n) Not found in HIBP breach data more than n times

Controlling the Threshold with uncompromised($threshold)

One underused feature: uncompromised() accepts an integer argument that sets the acceptable breach count. By default it’s 0, meaning any appearance in the HIBP database causes a failure.

// Fail only if password appears more than 5 times in breach data
Password::min(8)->uncompromised(5);

In some scenarios — like migrating existing users and enforcing new rules gradually — you might set a higher threshold temporarily. For new registrations, keep it at 0. Don’t get cute with that number.

Setting Application-Wide Defaults

Rather than repeating your password rules in every controller or form request, use Password::defaults() in your AppServiceProvider:

use Illuminate\Validation\Rules\Password;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Password::defaults(function () {
            $rule = Password::min(8);

            return $this->app->isProduction()
                ? $rule->mixedCase()->numbers()->symbols()->uncompromised()
                : $rule;
        });
    }
}

Now in your validation rules, just reference Password::defaults():

$request->validate([
    'password' => ['required', 'confirmed', Password::defaults()],
]);

This is the pattern Laravel’s own authentication scaffolding uses internally. One place to update, everywhere enforced. I can’t believe I spent years copy-pasting password rules across form requests before finding this.

Practical Integration Patterns for Real Applications

When you first integrate uncompromised(), there are a few real-world scenarios worth thinking through.

Registration vs. Password Reset vs. Profile Updates

You probably want uncompromised() on all three, but the user messaging matters. Laravel’s default validation error message is reasonable, but customizing it in your lang/en/validation.php or via trans() makes for a much better UX:

// In your custom Form Request
public function messages(): array
{
    return [
        'password.password' => 'This password has appeared in a data breach. Please choose a different one.',
    ];
}

Handling API Failures Gracefully

What happens if the HIBP API is unavailable? By default, Laravel’s uncompromised() will fail silently and let the validation pass — it won’t block your users because of a third-party outage. That’s the right call. A registration form that rejects valid users because of someone else’s uptime problems is a terrible trade-off, and I’m glad the framework takes that position by default.

If you want to be more aggressive, you can catch the underlying HTTP exception and handle it yourself. For most applications, though, the default resilience is fine.

Testing Locally Without Real API Calls

In your test suite, you don’t want real HTTP calls going out to HIBP. Use Laravel’s Http::fake() to mock the response:

use Illuminate\Support\Facades\Http;

Http::fake([
    'api.pwnedpasswords.com/*' => Http::response(
        "ABC123:5\nDEF456:1", // Simulate a match
        200
    ),
]);

Or simply use Http::preventStrayRequests() in your test setup to catch any unexpected outbound calls.

Built-in Validation Rule in Laravel to Check Security Policies You Might Be Missing

This HIBP check is the most visible gem, but digging through the Laravel validation documentation reveals several other underused built-in rules that don’t get nearly enough attention:

  • current_password — validates that the input matches the authenticated user’s current password (useful for “change password” flows)
  • prohibited_if / prohibited_unless — conditionally block fields based on other field values
  • exclude_if / exclude_unless — remove fields from validated data under conditions
  • decimal:min,max — validates decimal places, not just that something is numeric
  • mac_address — yes, it validates MAC addresses

The pattern here is the same every time: Laravel ships with more than most developers ever reach for. How many times have you written a custom rule that already existed in the framework? I know I have.

Where This Fits in an AI-Assisted Development Workflow

If you’re using GitHub Copilot, Cursor, or similar AI coding tools to scaffold Laravel applications, these tools frequently suggest basic min:8 password validation and nothing else. That’s a real gap. Adding Password::defaults() with uncompromised() to your boilerplate prompts, starter kits, or project templates means every app you spin up — AI-assisted or not — starts with a stronger security posture without extra thought.

Conclusion

TIL there’s a built-in validation rule in Laravel to check if passwords have been compromised is the kind of discovery that changes how you write registration and authentication flows going forward. The Password::uncompromised() rule is production-ready, privacy-preserving thanks to k-anonymity, and requires exactly zero additional dependencies.

Set it up once in AppServiceProvider via Password::defaults(), include uncompromised() as a non-negotiable, and you’re protecting your users from one of the most common attack vectors — credential stuffing — with a single method call. Finding this natively in the framework makes me want to do a proper audit of everything else sitting quietly in Laravel, waiting to be found.

Check the official Laravel validation docs and the Password rule source on GitHub — both are worth your time.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.