Safeguard
Application Security•

ASVS 5.0 V6.2.12: Check New Passwords Against Breaches

Reject passwords that appear in known breach data at registration and change. k anonymity lookup code, tests and pitfalls. Level 2 requirement.

Safeguard Security Team
4 min read

ASVS V6.2.12 requires that passwords submitted at registration or password change are checked against a set of breached passwords. It is a Level 2 requirement.

This is the check that connects your application to the real world of leaked credentials. A password can be long and unusual and still be public, because someone already used it and lost it in a breach.

Why it matters

Attackers replay passwords from earlier leaks. A password that has already been exposed is effectively public, however long it is.

Credential stuffing tools feed lists of exposed username and password pairs into thousands of sites. When users reuse a password, those lists succeed. Checking at creation time stops the user from choosing a value that is already in those lists, and reduces the value of any list an attacker holds.

The check is also an educational moment. The message tells the user that the password has appeared in a breach, which is far more convincing than a rule about symbols.

How to implement it

You can host a downloaded set of breach hashes, or call a range style API that never sees the full password. The k anonymity pattern sends only the first five characters of a SHA 1 hash and compares the rest locally.

import hashlib, requests

def is_breached(pw: str) -> bool:
    h = hashlib.sha1(pw.encode("utf-8")).hexdigest().upper()
    prefix, suffix = h[:5], h[5:]
    r = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}", timeout=3)
    r.raise_for_status()
    return any(line.split(":")[0] == suffix for line in r.text.splitlines())

Decide what happens when the service is unreachable and write it down. Many teams allow the password and log the miss; stricter teams block. Do the check over TLS and never log the password.

You have two realistic choices. The first is a local copy of a breach hash set. This keeps all checking inside your network but needs storage, updates and a lookup structure such as a Bloom filter or a sorted file. The second is a range query API that uses k anonymity. Your server hashes the password, sends only a short prefix, and compares the returned suffixes locally.

Whichever you pick, plan for failure and privacy.

  • Set a short timeout, and decide whether to fail open or closed. For registration, many teams fail open and queue a background re check.
  • Never log the password or the full hash.
  • Add the check to registration, change and reset, and consider re checking existing hashes when you receive news of a new breach, using the same set to flag accounts for a reset. V6.2.10 explains why this is better than calendar rotation.
async function breached(pw) {
  const sha1 = crypto.createHash("sha1").update(pw, "utf8").digest("hex").toUpperCase();
  const res = await fetch(`https://api.pwnedpasswords.com/range/${sha1.slice(0, 5)}`, {
    signal: AbortSignal.timeout(3000),
    headers: { "Add-Padding": "true" },
  });
  if (!res.ok) throw new Error("breach service unavailable");
  const text = await res.text();
  return text.split("\n").some(l => l.split(":")[0].trim() === sha1.slice(5));
}

The padding header asks the service to add decoy entries so response size reveals less.

How to test it

Try a password known to be in breach data, such as Password123! padded to your minimum length if needed, and confirm rejection with an explanation. Simulate a timeout and confirm the behavior matches your documented choice.

Use a fixture password you know is in breach data. Confirm that registration and change reject it, and that the response explains why. Stub the network call to return an error and confirm your fail open or fail closed rule. Add a test that checks no logging line contains the password or the hash, by capturing log output during the request.

Common mistakes

  • Sending the full password or full hash to a third party.

  • Checking at signup only.

  • Having no timeout, so registration hangs when the API is slow.

  • Ignoring the reset flow, which is the one attackers use to set an easy value.

  • Comparing the hash prefix instead of the suffix, which gives false matches.

  • Blocking users so aggressively that they cannot finish signup, with no hint about what to try.

Quick check

Why send only five hash characters?

It lets the service return candidates without learning the password you are checking.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.