DEV Community

Alex Spinov
Alex Spinov

Posted on

Have I Been Pwned Has a Free API — Check If Any Email Was in a Data Breach

In 2024, a breach exposed 26 billion records. Your email is probably in at least one breach.

Have I Been Pwned (HIBP) lets you check — and their password API is completely free. No key needed.

The APIs

HIBP has two types of access:

  1. Password API — Free, no key, unlimited
  2. Email breach API — Requires paid key ($3.50/month) or use the free website

Let's focus on what's free.

1. Check Passwords Without Sending Them (k-Anonymity)

This is brilliant: you send only the first 5 characters of the SHA-1 hash. HIBP returns all matching hashes. Your password never leaves your machine.

import hashlib
import requests

def check_password(password):
    """Check if a password has been in a data breach.
    Uses k-anonymity: only first 5 chars of hash are sent."""
    sha1 = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()
    prefix = sha1[:5]
    suffix = sha1[5:]

    resp = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}')

    for line in resp.text.splitlines():
        hash_suffix, count = line.split(':')
        if hash_suffix == suffix:
            return {'pwned': True, 'count': int(count)}

    return {'pwned': False, 'count': 0}

# Check some passwords
for pwd in ['password123', 'correct-horse-battery-staple', 'j&82kL!mz9']:
    result = check_password(pwd)
    status = f'PWNED {result["count"]:,} times!' if result['pwned'] else 'Safe'
    print(f'  {pwd:<35} {status}')
Enter fullscreen mode Exit fullscreen mode

Output:

  password123                         PWNED 247,516 times!
  correct-horse-battery-staple        PWNED 131 times!
  j&82kL!mz9                          Safe
Enter fullscreen mode Exit fullscreen mode

2. Build a Password Strength Checker

def password_audit(password):
    """Comprehensive password check."""
    issues = []

    if len(password) < 12:
        issues.append(f'Too short ({len(password)} chars, need 12+)')
    if password.lower() == password:
        issues.append('No uppercase letters')
    if not any(c.isdigit() for c in password):
        issues.append('No numbers')
    if not any(c in '!@#$%^&*()_+-=[]{}|;:,.<>?' for c in password):
        issues.append('No special characters')

    # Check breach database
    breach = check_password(password)
    if breach['pwned']:
        issues.append(f'Found in {breach["count"]:,} data breaches!')

    score = max(0, 5 - len(issues))

    return {
        'score': f'{score}/5',
        'issues': issues,
        'verdict': 'Strong' if score >= 4 else 'Moderate' if score >= 2 else 'Weak'
    }

result = password_audit('MyP@ssw0rd!')
print(f"Score: {result['score']}{result['verdict']}")
for issue in result['issues']:
    print(f"  - {issue}")
Enter fullscreen mode Exit fullscreen mode

3. Batch Check All Your Passwords

import time

def audit_password_file(filepath):
    """Check a list of passwords (one per line)."""
    with open(filepath) as f:
        passwords = [line.strip() for line in f if line.strip()]

    pwned_count = 0
    for pwd in passwords:
        result = check_password(pwd)
        if result['pwned']:
            pwned_count += 1
            print(f"  !!! '{pwd[:3]}***' found in {result['count']:,} breaches")
        time.sleep(0.1)  # Be polite

    print(f"\n{pwned_count}/{len(passwords)} passwords found in breaches")
Enter fullscreen mode Exit fullscreen mode

4. Add to Registration Forms

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/api/check-password', methods=['POST'])
def check():
    password = request.json.get('password', '')
    result = check_password(password)

    if result['pwned']:
        return jsonify({
            'safe': False,
            'message': f'This password appeared in {result["count"]:,} data breaches. Choose a different one.'
        }), 400

    return jsonify({'safe': True, 'message': 'Password not found in known breaches'})
Enter fullscreen mode Exit fullscreen mode

Frontend:

async function validatePassword(password) {
  const resp = await fetch('/api/check-password', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({password})
  });
  const data = await resp.json();

  if (!data.safe) {
    document.getElementById('warning').textContent = data.message;
  }
}
Enter fullscreen mode Exit fullscreen mode

Rate Limits

  • Password API: No authentication required
  • Suggested: 1 request per 1500ms for bulk operations
  • The API uses Cloudflare, so excessive requests may get rate-limited

Privacy

The k-anonymity model means:

  • Your full password hash is never sent to the server
  • Only the first 5 characters of the SHA-1 hash are transmitted
  • The API returns ~500 matching suffixes — your password is hidden in the crowd
  • Troy Hunt (the creator) has a great writeup on the security model

Combine With


Building free security tools. More: GitHub | Writing: Spinov001@gmail.com

Top comments (0)