Protect your login from brute force and leaked passwords
Why a password field and a reCAPTCHA checkbox aren't enough, and the layers that stop brute force and credential stuffing. Free options first.
In short Attackers rarely guess passwords anymore: they already have them from old leaks. Put Cloudflare Turnstile on your login and verify it on the server, add a risk engine such as ALTCHA Sentinel or reCAPTCHA Enterprise, limit failed attempts per account, and offer two-factor login.
Your login page is the busiest door on your website, and most of what knocks on it isn’t human. Bots try thousands of email and password pairs an hour. They don’t need to be clever. They only need one of your users to have reused a password somewhere else.
Plenty of Algerian websites still protect their login with a password field and nothing else, or with a reCAPTCHA checkbox that looks safe and isn’t. This post explains what attackers actually do, why a CAPTCHA on its own doesn’t stop them, and the layers that do. The first ones are free.
Two attacks, one door
Brute force: guessing
The bot picks an account and tries passwords: 123456, azerty, the user’s name followed by a birth year, a dictionary of the most common passwords. Brute force is noisy. One account collects thousands of failed attempts, which is easy to see if anyone is looking.
Credential stuffing: using passwords that already leaked
This one is worse, because nothing is guessed. When a website is breached, its list of emails and passwords ends up sold, or shared for free on forums and Telegram channels. These lists, often called combolists, hold millions of real pairs. Attackers feed them into other websites’ login forms, betting that people use the same password everywhere. Most people do.
A stuffing attack can succeed on the first try for each account, so there is no wall of failed logins to warn you. A strict password rule doesn’t help either: the attacker isn’t guessing the password, they already have it.
This isn’t a distant problem. In April 2025, hacker groups claimed to publish tens of gigabytes taken from Algerian public institutions. And every large breach of an international service, the kind with hundreds of millions of accounts, includes Algerian users too.
Why reCAPTCHA on its own isn’t enough
A CAPTCHA proves that something solved a puzzle. It doesn’t prove that a person did, and it says nothing about whether the password was stolen.
- Solving services exist. OWASP, the main reference for web security, says it directly: “CAPTCHAs are not perfect, and in many cases tools or services exist that can be used to break them.” Some solve challenges with AI, others with paid human workers, for a fraction of a cent each.
- The widget is often decoration. It gives the browser a token, and your server has to send that token back to Google or Cloudflare to check it. Many sites skip that step. A bot then ignores the widget, posts the form straight to your server, and gets in. Cloudflare’s own documentation warns that the widget alone does not protect your forms.
- reCAPTCHA v3 only returns a score. If your code doesn’t act on a low score, nothing is blocked.
- The free tier shrank. reCAPTCHA’s free allowance is now 10,000 checks a month per organization.
The answer isn’t to drop CAPTCHAs. It’s to stop treating one as the whole defense.
The layers that stop it
No single tool closes the door. Each layer below removes part of the attackers, and together they make an attack slow, expensive and visible. Start at the top: the first layers cost nothing.
1. Put the site behind Cloudflare
Cloudflare’s free plan sits in front of your server and sees every request first. Two of its features matter for logins.
Rate limiting caps how many requests one visitor can send. A rule like “no more than 5 login attempts per minute from one IP address” stops the clumsy bots. The free plan includes one rate limiting rule: spend it on your login URL. On WordPress, also block or limit xmlrpc.php, a second login door that bots love.
Leaked credentials detection checks the email and password in login requests against Cloudflare’s database of leaked credentials, so your rules can react to a match. It’s on by default on the free plan, with a ready-made rule template.
IP-based limits won’t stop a serious attacker, who spreads requests over thousands of home internet addresses rented through proxy networks. OWASP says blocking by IP “should not be used as the sole or primary defense”. It’s the first filter, not the last.
2. Add Cloudflare Turnstile, and verify it on the server
Turnstile is Cloudflare’s replacement for CAPTCHAs. Most visitors never see a puzzle: it runs checks in the browser and only asks for a click when something looks wrong. It’s free, with unlimited verifications and up to 20 widgets per account.
The step people skip is the server check. When the form is submitted, the browser sends a token in the cf-turnstile-response field. Your server sends that token to Cloudflare and refuses the login unless Cloudflare confirms it. Each token works once and expires after 5 minutes.
function turnstile_ok(string $token, string $ip, string $secret): bool {
$ch = curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => $secret,
'response' => $token,
'remoteip' => $ip,
]),
]);
$body = curl_exec($ch);
curl_close($ch);
$data = json_decode($body ?: '', true);
return ($data['success'] ?? false) === true;
}
// At the top of the login handler, before checking the password:
$token = $_POST['cf-turnstile-response'] ?? '';
if (!turnstile_ok($token, $_SERVER['REMOTE_ADDR'], getenv('TURNSTILE_SECRET'))) {
http_response_code(403);
exit('Verification failed. Please try again.');
}
Keep the secret key on the server, in an environment variable, never in the page. When the site runs behind Cloudflare, the visitor’s real address is in $_SERVER['HTTP_CF_CONNECTING_IP'], not REMOTE_ADDR.
3. Add a risk engine that knows about leaked passwords
Turnstile tells you whether a browser looks automated. It doesn’t tell you that this exact email and password appeared in a leak last year, or that an account that always logs in from Oran is suddenly logging in from another continent at 3 a.m. That’s the job of a risk engine. Two good options:
reCAPTCHA Enterprise, and its smaller Premium tier, add password defense: a check of whether the username and password pair is known to be leaked, done with cryptography that keeps the password private. Enterprise also scores every login for account-takeover risk. Premium is free up to 10,000 checks a month, then an $8 flat fee up to 100,000, then $1 per 1,000. A password check counts as one extra check.
ALTCHA Sentinel is the option if you’d rather not send your visitors to Google. You host it yourself. It adjusts difficulty to risk, from an invisible proof-of-work check to an interactive challenge, and learns from your own traffic. It costs €99 a month for the Professional plan, a flat licence with no price per check. Its core widget, ALTCHA, is free and open source if the proof-of-work part is all you need.
For anything that handles money or personal data, this is the setup I recommend: Turnstile in front, one risk engine behind it.
4. Count failed logins per account, not only per IP
An attacker can change IP address on every request. They can’t change which account they’re attacking. Keep a counter of failed attempts on each account, as OWASP recommends: failures should be “associated with the account itself, rather than the source IP address”.
After a few failures, add a delay that grows with each new one. After more, ask for a CAPTCHA or a second factor, or lock the account for a few minutes. Avoid locking accounts for good: attackers can use that to lock your real users out on purpose. NIST, the US standards body, sets the upper bound at 100 consecutive failed attempts before an account must be blocked; most sites should react much sooner.
Count per IP as well: one address trying hundreds of different emails is credential stuffing in progress.
5. Give the same answer for every failure
“Wrong password” and “No account with this email” look helpful, but together they tell an attacker which emails are registered on your site. OWASP says the login must respond with a generic error message whatever went wrong. Show “Email or password is incorrect” every time, and make sure the response time and HTTP status don’t give the difference away either.
6. Refuse passwords that have already leaked
NIST requires checking new passwords against a blocklist that includes passwords from previous breaches. The free Pwned Passwords API from Have I Been Pwned makes this easy, needs no account or key, and never sees the password: your server sends only the first 5 characters of its SHA-1 hash, gets back every leaked hash that starts with them, and compares the rest locally.
function password_is_breached(string $password): bool {
$hash = strtoupper(sha1($password));
$prefix = substr($hash, 0, 5); // only these 5 characters leave your server
$suffix = substr($hash, 5);
$ch = curl_init("https://api.pwnedpasswords.com/range/$prefix");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => ['User-Agent: my-site-password-check', 'Add-Padding: true'],
]);
$body = curl_exec($ch);
curl_close($ch);
if ($body === false) {
return false; // API unreachable: don't block the user, just skip the check
}
foreach (explode("\n", $body) as $line) {
[$candidate, $count] = array_pad(explode(':', trim($line)), 2, '0');
if ($candidate === $suffix && (int) $count > 0) {
return true;
}
}
return false;
}
Run it at signup and whenever a password changes. 123456 and azerty both come back as leaked; a long random password doesn’t.
NIST’s current rules also say what not to do. Don’t force a mix of symbols and capitals, and don’t make people change their password every few months. Ask for length instead: at least 15 characters when the password is the only factor, at least 8 when two-factor login is on, and allow at least 64.
7. Offer two-factor login, and require it for admins
This is the strongest layer of all. OWASP calls multi-factor authentication “by far the best defense” against these attacks, citing Microsoft’s finding that it would have stopped 99.9% of account compromises. With it, a stolen password is no longer enough.
Authenticator apps and passkeys are the best choices. Codes by SMS are weaker, but still far better than nothing. You don’t have to ask on every login: asking only when something is new, such as a new device or a new country, protects accounts without annoying regular users. Admin and staff accounts should always require it.
8. Store passwords properly and watch your logs
Store passwords with a slow hash made for the job. In PHP, that’s password_hash($password, PASSWORD_ARGON2ID) or PASSWORD_DEFAULT, checked with password_verify(). Never store plain MD5 or SHA-1: a leak of your own database would then turn into someone else’s combolist.
Then watch for the signs of an attack:
- a sudden jump in failed logins;
- many different emails tried from the same address;
- a correct password followed by a failed second factor, which means someone else knows that password;
- a CAPTCHA solve rate that is suspiciously high, a sign that a solving service is at work.
Email users when their account is accessed from a new device. They’ll often notice an attack before you do.
What we do at DZBuild
At DZBuild, merchants run real businesses: their orders, customers and money sit behind the login. So DZBuild runs behind ALTCHA Sentinel on the Enterprise plan. It’s self-hosted, so the bot checks run on our own infrastructure instead of a third party’s, and the challenge gets harder as the risk goes up. Some of our services use Cloudflare Turnstile as well.
The layers above are the reasoning behind that setup, and they apply to any site, whatever its size.
If you only do three things this week
- Put your site on Cloudflare’s free plan and add a rate limiting rule on the login URL.
- Add Turnstile to the login form, and check the token on the server.
- Refuse leaked passwords at signup with the Pwned Passwords check above.
Then add a risk engine and two-factor login. None of this makes a site impossible to attack. It makes yours a bad target, and bots move on to easier ones.
Sources
- OWASP, Credential Stuffing Prevention Cheat Sheet
- OWASP, Authentication Cheat Sheet
- NIST, SP 800-63B Digital Identity Guidelines: Authentication
- Cloudflare, Turnstile server-side validation and Turnstile plans
- Cloudflare, Leaked credentials detection
- Google Cloud, reCAPTCHA tiers compared
- ALTCHA, Sentinel and plans
- Have I Been Pwned, Pwned Passwords API