RestingOwl owl logo RestingOwl

Breached Password Detectionwith HaveIBeenPwned & k-Anonymity

Quick Answer: Hash the user's password with SHA-1, take the first 5 characters, and send only those to the HaveIBeenPwned API. The API returns all known breached hashes that share that prefix. Check whether your full hash appears in the list. The raw password and the full hash never leave your server: this is k-anonymity.

Even a perfectly implemented authentication system can be undermined by one thing: users choosing passwords that have already been exposed in data breaches elsewhere. A user who sets their password to one leaked from a previous breach is vulnerable to credential stuffing attacks: automated attempts using known username/password pairs harvested from other breaches. The HaveIBeenPwned Pwned Passwords API gives you a way to catch this at registration and login, without ever sending the user's password to a third party.

What Is a Breached Password and Why Does It Matter?

A breached password is one that has appeared in a publicly known data breach: a database leak, a credential dump, or a security incident where user credentials were exposed. The HaveIBeenPwned project, maintained by Troy Hunt, aggregates these breaches and exposes the compromised passwords through an API. As of 2026, the database contains over a billion breached passwords.

The risk is specific: an attacker who obtains a breached credential list doesn't need to crack passwords: they already know them. They test those credentials against other services. If your users reuse passwords, and most do, a breach on another platform becomes an attack vector on yours. Checking for breached passwords at registration time blocks the most common credential stuffing targets before they can be used.

OWASP's Authentication Cheat Sheet explicitly recommends checking new passwords against breach databases as part of a robust credential policy: "Ensure that credentials are not found in known breached datasets."

How Does the HaveIBeenPwned API Work?

The Pwned Passwords API uses a Range search endpoint. You send the first 5 characters of the SHA-1 hash of the password. The API responds with a list of all SHA-1 hash suffixes in its database that share that prefix, along with how many times each appeared in known breaches.

Your client then reconstructs the full hash by prepending the 5-character prefix to each returned suffix and checks whether the user's full SHA-1 hash is in that list. If it is, the password has been breached. If it is not, the password is unknown to the database.

The API never receives the full hash: only the first 5 characters. A 5-character hex prefix matches thousands of different possible full hashes, so the API cannot determine which specific password you are checking. This is the k-anonymity property.

What Is k-Anonymity and Why Is It Critical?

k-anonymity is a privacy property. In this context, it means that any single API request is indistinguishable from thousands of other requests that share the same 5-character prefix. The HaveIBeenPwned server sees the prefix: but that prefix corresponds to a minimum of k different possible full hashes, so it cannot determine which specific password triggered the request.

ApproachWhat Leaves Your ServerPrivacy Risk
Send full passwordThe raw passwordCritical: third party sees the password
Send full SHA-1 hashThe complete hashHigh: hash can be reversed or matched
Send first 5 chars of SHA-1 (k-anonymity)5-character prefix onlyMinimal: thousands of passwords share any prefix

The k-anonymity approach means you can use the HaveIBeenPwned API in production without violating user trust or creating a privacy liability. The password never leaves your process: only an ambiguous prefix does.

Implementing the Check in Node.js

The implementation has three steps: hash the password with SHA-1, send the first 5 characters to the API, then check the response. Using Node.js built-in modules:

  • Step 1: Hash: const hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
  • Step 2: Split: const prefix = hash.slice(0, 5); const suffix = hash.slice(5);
  • Step 3: Request: GET https://api.pwnedpasswords.com/range/{prefix}
  • Step 4: Check: Parse the response lines (each is SUFFIX:COUNT) and look for a line where the suffix matches your suffix variable.
  • Step 5: Decide: If found, the password is breached. Return an error and ask the user to choose a different password.

If you use OwlAuth, this entire flow is handled automatically. The library makes the k-anonymous API call, parses the response, and returns a structured result: the raw password never touches the HaveIBeenPwned infrastructure, and the check adds under 100ms of latency in typical conditions.

Where in the Auth Flow Should You Check?

The check should happen at two points:

Trigger PointActionUser Experience
Registration: password creationBlock if breached, show friendly errorUser selects a different password before account is created
Password change / resetBlock if breached, show friendly errorUser cannot cycle to a known-bad password
Login (optional)Warn and prompt password changeExisting users with breached passwords are notified over time

Checking at login is optional and has UX trade-offs: blocking a login because the password is breached can be confusing if the user doesn't know why. A common pattern is to allow login but immediately prompt a password change with a clear explanation. The mandatory check points are registration and password reset.

What OWASP Says About Compromised Credentials

OWASP's Authentication Cheat Sheet includes compromised credential checking as a recommended control under password policy. The ASVS (Application Security Verification Standard) 5.0 includes it in Level 1 requirements: the baseline that all applications should meet:

  • Verify that passwords are checked against known breached password lists
  • Reject passwords that match common patterns, dictionary words, or context-specific terms (username, email, app name)
  • Provide clear, non-generic error messages when a password is rejected so the user understands what to change

The breached password check is not an advanced security feature: it is a baseline requirement. The HaveIBeenPwned API makes it free and straightforward to implement. There is no good reason to skip it.

Rate Limits and Caching Considerations

  • The HaveIBeenPwned Pwned Passwords API is free and has no official rate limit, but the API documentation recommends caching responses to the same prefix to reduce load.
  • In practice, common password prefixes will be requested frequently. A short-lived in-memory or Redis cache keyed on the 5-character prefix improves performance without compromising privacy: you are caching the prefix response, not the user's password.
  • The API also supports a Add-Padding header that adds random padding to responses, further improving privacy against traffic analysis.

References

  1. 1OwlAuth: Built-in HaveIBeenPwned Integration for Node.js
  2. 2OWASP Authentication Cheat Sheet: Compromised Credentials
  3. 3HaveIBeenPwned Pwned Passwords API

Q&A Section

No. The SHA-1 hash is computed temporarily in memory solely for the purpose of the HaveIBeenPwned check. It is never stored. Your actual stored password hash should use bcrypt, Argon2, or scrypt: a slow, salted algorithm designed for password storage. SHA-1 is appropriate here only because it matches the format of the HaveIBeenPwned database.
Be direct but non-alarming. Something like: 'This password has appeared in known data breaches and is not safe to use. Please choose a different password.' Avoid saying the password was found on 'our system': it was found in an external breach database, not your own.
Yes. HaveIBeenPwned makes the full Pwned Passwords database available for download. You can download the SHA-1 hash list, store it locally, and query it without any outbound API calls. This is useful for air-gapped environments or applications with strict data residency requirements.
A hard block is not recommended as a first step: it creates confusion and support burden. The better pattern is to allow login but immediately redirect to a forced password change flow with a clear explanation. Reserve hard blocking for applications with elevated security requirements.
Copied!