PHP · Password breach checks
Check breached passwords in PHP before hashing
PHP gives you password_hash and password_verify for storage, and a breach check slots in right before password_hash during signup or a password change. The Finowl endpoint receives the plaintext over HTTPS, hashes it with SHA-1, searches by a 5-character prefix with padding, and returns breached and count. Finowl processes the password transiently and never logs, stores or returns it.
The function below keeps the password out of every message it can produce. Exceptions include the HTTP status and error code but never the input, and the JSON body is built inside the function so it is not stored on an object that a debug dump might print. For the demo, the password is read from an environment variable rather than a command-line argument, which would be visible in the process list.
Steps
- 1
Enable the cURL extension and set FINOWL_API_KEY for PHP-FPM or the CLI.
- 2
Try the script with CANDIDATE_PASSWORD set in your environment and run php check-password.php.
- 3
Call breachedPasswordCount in your registration handler after your own length rules pass.
- 4
Return a form error when the count is above zero, and only then call password_hash.
<?php
declare(strict_types=1);
/** Returns how often $password appears in breach data. Never log $password. */
function breachedPasswordCount(#[SensitiveParameter] string $password): int
{
for ($attempt = 0; $attempt < 3; $attempt++) {
$retryAfter = 1;
$ch = curl_init('https://api.finowl.co/v1/breaches/password');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('FINOWL_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['password' => $password], JSON_THROW_ON_ERROR),
CURLOPT_HEADERFUNCTION => function ($ch, string $header) use (&$retryAfter): int {
if (stripos($header, 'Retry-After:') === 0) {
$retryAfter = max(1, (int) trim(substr($header, 12)));
}
return strlen($header);
},
]);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException('Password check network error: ' . curl_error($ch));
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$data = json_decode($body, true) ?? [];
$code = $data['error']['code'] ?? 'unknown_error';
switch (true) {
case $status === 200:
return $data['breached'] ? (int) $data['count'] : 0;
case $status === 400:
throw new InvalidArgumentException("Password failed validation: {$code}");
case $status === 401:
throw new RuntimeException('Unauthorized: check FINOWL_API_KEY');
case $status === 429 && $code === 'rate_limited':
sleep($retryAfter);
break;
default:
throw new RuntimeException("Password check failed: {$status} {$code}");
}
}
throw new RuntimeException('Password check still rate limited');
}
$count = breachedPasswordCount((string) getenv('CANDIDATE_PASSWORD'));
echo $count > 0
? 'Seen ' . number_format($count) . ' times in breach data. Choose another password.' . PHP_EOL
: 'Not found in breach data.' . PHP_EOL;IN PRODUCTION
Production tips
The #[SensitiveParameter] attribute, available from PHP 8.2, redacts the password from stack traces. On PHP 8.0 and 8.1 the attribute is ignored, so make sure error pages never show argument values.
Do not enable CURLOPT_VERBOSE in production for this request. Verbose output can be written to error logs and may include request details you do not want stored.
Choose how to behave on a timeout. Allowing the signup and rechecking the password at next login is often friendlier than blocking a real user because of a network issue.
Each lookup costs 1 credit, with 100 free credits every month. See pricing and error codes.
A FEW GOOD QUESTIONS
Common questions
Related guides
- Check email breaches in PHP with cURL
- Predict gender from names in PHP 8 with cURL
- Check breached passwords in Python at signup
- Screen breached passwords in Node.js
- Check breached passwords in TypeScript
- Check breached passwords in Go before storing them
- Breached password checks for fintech accounts
- Breached password screening for e-commerce accounts
- Breached password checks for SaaS signup and resets
Check breached passwords in PHP before hashing
Grab an API key, paste the example and run it. 100 free credits every month.