DEV Community

Cover image for How to Build a HaveIBeenPwned Breach Auditor in Python
ShadowStrike
ShadowStrike

Posted on

How to Build a HaveIBeenPwned Breach Auditor in Python

Version 1.0.0

Data breaches happen constantly. When credentials from one breach get reused in credential-stuffing attacks against other services, the ripple effect can last years. That's why checking whether an email address or password has appeared in a known breach is a routine first step in any security assessment.

HaveIBeenPwned (HIBP) maintains one of the most comprehensive breach databases available, with over 12 billion compromised accounts indexed. In this tutorial, you'll build a Python CLI tool that checks email addresses and passwords against that database using the HIBP API, with proper k-anonymity implementation to protect privacy.

What You'll Build

A command-line Python tool called hibp_auditor.py that:

  • Checks passwords using k-anonymity (your password never leaves your machine) — works without any API key
  • Checks email addresses against the HIBP breach database (requires paid API subscription)
  • Handles API rate limiting gracefully
  • Outputs results to console and optionally to a timestamped report file

Note: Password checking is free and works immediately. Email breach checking requires a paid HIBP API subscription (pricing started ~2024).

Prerequisites

  • Python 3.6 or later
  • requests library (pip install requests)
  • No API key needed for password checking (uses k-anonymity)
  • Optional: Paid HIBP API subscription for email breach checking (haveibeenpwned.com/API/Key)

Understanding K-Anonymity

Before diving into code, it's worth understanding why password checking with HIBP is safe - because the implementation uses k-anonymity.

The problem: If you send your password to an API to check if it's compromised, you're... sending your password to an API. That's not great.

The solution: Instead of sending the full password, HIBP's Pwned Passwords API uses a clever technique:

  1. You hash your password locally using SHA-1
  2. You send only the first 5 characters of that hash to the API
  3. The API returns all pwned password hashes that start with those 5 characters
  4. You check locally whether your full hash is in that list

This means the API never sees your actual password or even your full hash. The first 5 characters of a SHA-1 hash match thousands of different passwords, so the API can't determine which specific password you're checking.

This is k-anonymity: your query is indistinguishable from k other possible queries, where k is large enough to preserve privacy.

The Complete Script

Here's the full implementation:

#!/usr/bin/env python3
"""
HaveIBeenPwned Breach Auditor
Purpose: Check email addresses and passwords against the HIBP breach database
Author: ShadowStrike (Strategos)
License: MIT
"""

import argparse
import hashlib
import requests
import sys
import time
from datetime import datetime

# HIBP API endpoints
HIBP_BREACH_API = "https://haveibeenpwned.com/api/v3/breachedaccount/{}"
HIBP_PASSWORD_API = "https://api.pwnedpasswords.com/range/{}"

def check_email_breaches(email, api_key=None):
    """
    Check if an email address appears in known data breaches.

    Args:
        email: Email address to check
        api_key: HIBP API key (required for email checks)

    Returns:
        List of breach dictionaries or None if error
    """
    if not api_key:
        print("[ERROR] Email breach checking requires an HIBP API key")
        print("[INFO] Get a free key at: https://haveibeenpwned.com/API/Key")
        return None

    url = HIBP_BREACH_API.format(email)
    headers = {
        'hibp-api-key': api_key,
        'user-agent': 'HIBP-Breach-Auditor'
    }

    try:
        response = requests.get(url, headers=headers, timeout=10)

        if response.status_code == 200:
            return response.json()
        elif response.status_code == 404:
            return []  # No breaches found (good news!)
        elif response.status_code == 429:
            print("[ERROR] Rate limit exceeded - wait and try again")
            return None
        else:
            print(f"[ERROR] API returned status code: {response.status_code}")
            return None

    except requests.exceptions.RequestException as e:
        print(f"[ERROR] Network error: {e}")
        return None

def check_password_pwned(password):
    """
    Check if a password appears in known breaches using k-anonymity.

    This uses the Pwned Passwords API with k-anonymity - only the first 5 
    characters of the SHA-1 hash are sent to the API, protecting privacy.

    Args:
        password: Password to check (never sent to API in plain text)

    Returns:
        Tuple of (is_pwned: bool, count: int) or (None, None) if error
    """
    # Hash the password locally
    sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()

    # Send only the first 5 characters
    prefix = sha1_hash[:5]
    suffix = sha1_hash[5:]

    url = HIBP_PASSWORD_API.format(prefix)

    try:
        response = requests.get(url, timeout=10)

        if response.status_code == 200:
            # Parse the response - each line is "suffix:count"
            hashes = response.text.split('\r\n')

            for hash_line in hashes:
                if ':' in hash_line:
                    hash_suffix, count = hash_line.split(':')
                    if hash_suffix == suffix:
                        return (True, int(count))

            # Hash not found in response = password not pwned
            return (False, 0)

        elif response.status_code == 429:
            print("[ERROR] Rate limit exceeded")
            return (None, None)
        else:
            print(f"[ERROR] API returned status code: {response.status_code}")
            return (None, None)

    except requests.exceptions.RequestException as e:
        print(f"[ERROR] Network error: {e}")
        return (None, None)

def format_breach_info(breach):
    """Format a breach dictionary into readable output"""
    name = breach.get('Name', 'Unknown')
    domain = breach.get('Domain', 'N/A')
    breach_date = breach.get('BreachDate', 'Unknown')
    pwn_count = breach.get('PwnCount', 0)
    data_classes = ', '.join(breach.get('DataClasses', []))

    return f"""
  Breach: {name}
  Domain: {domain}
  Date: {breach_date}
  Accounts: {pwn_count:,}
  Data: {data_classes}
"""

def main():
    parser = argparse.ArgumentParser(
        description='Check email addresses and passwords against HaveIBeenPwned database',
        epilog='Example: python hibp_auditor.py --email test@example.com --api-key YOUR_KEY'
    )

    parser.add_argument('--email', type=str,
                        help='Email address to check for breaches')
    parser.add_argument('--password', type=str,
                        help='Password to check (uses k-anonymity - safe)')
    parser.add_argument('--api-key', type=str,
                        help='HIBP API key (required for email checks)')
    parser.add_argument('--output', type=str,
                        help='Write results to file (default: console only)')

    args = parser.parse_args()

    if not args.email and not args.password:
        parser.print_help()
        sys.exit(1)

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    results = []
    results.append(f"HIBP Breach Audit Report - {timestamp}")
    results.append("=" * 60)
    results.append("")

    # Check email if provided
    if args.email:
        print(f"\n[*] Checking email: {args.email}")
        results.append(f"Email: {args.email}")

        breaches = check_email_breaches(args.email, args.api_key)

        if breaches is None:
            results.append("  Status: ERROR - Could not complete check")
        elif len(breaches) == 0:
            print("[OK] No breaches found - this email is clean!")
            results.append("  Status: CLEAN - No breaches found")
        else:
            print(f"[WARNING] Found in {len(breaches)} breach(es):")
            results.append(f"  Status: COMPROMISED - Found in {len(breaches)} breach(es)")
            results.append("")

            for breach in breaches:
                breach_info = format_breach_info(breach)
                print(breach_info)
                results.append(breach_info)

        results.append("")

        # Rate limiting courtesy
        if args.password:
            time.sleep(1.5)

    # Check password if provided
    if args.password:
        print(f"\n[*] Checking password (using k-anonymity)...")
        results.append("Password: [REDACTED]")

        is_pwned, count = check_password_pwned(args.password)

        if is_pwned is None:
            results.append("  Status: ERROR - Could not complete check")
        elif is_pwned:
            print(f"[WARNING] Password found in {count:,} breaches!")
            print("[ADVICE] This password is compromised - change it immediately")
            results.append(f"  Status: PWNED - Found in {count:,} breaches")
            results.append("  Advice: Change this password immediately")
        else:
            print("[OK] Password not found in known breaches")
            results.append("  Status: CLEAN - Not found in known breaches")

        results.append("")

    # Write to file if requested
    if args.output:
        try:
            with open(args.output, 'w', encoding='utf-8') as f:
                f.write('\n'.join(results))
            print(f"\n[*] Report written to: {args.output}")
        except IOError as e:
            print(f"\n[ERROR] Could not write to file: {e}")

    print("\n[*] Audit complete")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

How to Use It

Installation

# Install dependencies
pip install requests


# Download the script
# (or clone from GitHub - link at end of tutorial)

# Linux/Mac only - make executable
chmod +x hibp_auditor.py

# Windows - no chmod needed, just run with python
python hibp_auditor.py --help
Enter fullscreen mode Exit fullscreen mode

Security Note: Read the Code Before Running It

Before executing this or any Python script from the internet:

  1. Read the source code completely — every function, every API call, every file operation
  2. Verify the logic — does it do what it claims and nothing else?
  3. Check for risks — unexpected network calls, file access, credential storage

Apply the ABC principle:

  • Assume nothing
  • Believe nothing
  • Check everything

Never execute code you haven't personally reviewed and understood, regardless of where it came from. You are the final security control.

Check a Password (No API Key Required)

python hibp_auditor.py --password "password123"
Enter fullscreen mode Exit fullscreen mode

Sample output:

[*] Checking password (using k-anonymity)...
[WARNING] Password found in 2,254,650 breaches!
[ADVICE] This password is compromised - change it immediately

[*] Audit complete
Enter fullscreen mode Exit fullscreen mode

Or with a strong password:

[*] Checking password (using k-anonymity)...
[OK] Password not found in known breaches

[*] Audit complete
Enter fullscreen mode Exit fullscreen mode

Save Results to File

python hibp_auditor.py --email test@example.com --api-key YOUR_KEY --output report.txt
Enter fullscreen mode Exit fullscreen mode

Check an Email Address (Requires Paid API Key)

Note: Email breach checking requires a paid HIBP API subscription. If you don't have an API key, the tool will show an error and direct you to the HIBP website.

python hibp_auditor.py --email test@example.com --api-key YOUR_API_KEY
Enter fullscreen mode Exit fullscreen mode

Sample output (with valid API key):

[*] Checking email: test@example.com
[WARNING] Found in 3 breach(es):

  Breach: Adobe
  Domain: adobe.com
  Date: 2013-10-04
  Accounts: 152,445,165
  Data: Email addresses, Password hints, Passwords, Usernames

  Breach: LinkedIn
  Domain: linkedin.com
  Date: 2012-05-05
  Accounts: 164,611,595
  Data: Email addresses, Passwords
Enter fullscreen mode Exit fullscreen mode

Code Walkthrough

Email Breach Checking

The check_email_breaches() function hits the HIBP API v3 endpoint:

url = HIBP_BREACH_API.format(email)
headers = {
    'hibp-api-key': api_key,
    'user-agent': 'HIBP-Breach-Auditor'
}
response = requests.get(url, headers=headers, timeout=10)
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Requires an API key (free for reasonable use)
  • Returns HTTP 404 if no breaches found (which we treat as good news)
  • Returns HTTP 429 if rate-limited (wait and retry)
  • Returns JSON array of breach objects if compromised

Password Checking with K-Anonymity

The check_password_pwned() function implements the k-anonymity protocol:

# Hash locally
sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()

# Send only first 5 chars
prefix = sha1_hash[:5]
suffix = sha1_hash[5:]

url = HIBP_PASSWORD_API.format(prefix)
response = requests.get(url, timeout=10)

# Check if our full hash is in the response
for hash_line in hashes:
    hash_suffix, count = hash_line.split(':')
    if hash_suffix == suffix:
        return (True, int(count))
Enter fullscreen mode Exit fullscreen mode

Why this is safe:

  1. Password is hashed locally with SHA-1
  2. Only the first 5 characters of the hash are sent
  3. API returns ~500-1000 hash suffixes matching that prefix
  4. We check locally if our full hash is in that list
  5. The API never learns which specific hash we're checking

Rate Limiting Courtesy

The HIBP API has rate limits. The script implements courtesy delays:

if args.password:
    time.sleep(1.5)  # Wait between email and password checks
Enter fullscreen mode Exit fullscreen mode

For production use checking multiple emails, implement exponential backoff when hitting 429 responses.

Real-World Use Cases

Small team audit: Check all company email addresses to see who's been compromised:

for email in alice@company.com bob@company.com charlie@company.com; do
  python hibp_auditor.py --email $email --api-key YOUR_KEY --output report_$email.txt
  sleep 2  # Rate limiting courtesy
done
Enter fullscreen mode Exit fullscreen mode

Password policy enforcement: Check common weak passwords against HIBP before allowing them in your system:

is_pwned, count = check_password_pwned(user_password)
if is_pwned and count > 100:
    return "This password appears in known breaches - choose another"
Enter fullscreen mode Exit fullscreen mode

Incident response: When investigating a suspected breach, check if credentials have appeared in public dumps:

python hibp_auditor.py --email suspicious@victim.com --api-key YOUR_KEY
Enter fullscreen mode Exit fullscreen mode

Security awareness training: Demonstrate to users how common their passwords are:

python hibp_auditor.py --password "password123"
# Shows: "Found in 9,238,454 breaches!"
Enter fullscreen mode Exit fullscreen mode

Extending the Script

Bulk email checking:
Add a --email-list parameter that reads from a CSV file and checks multiple addresses with proper rate limiting.

Domain-wide audit:
Integrate with your company directory (LDAP, Azure AD) to audit all @yourcompany.com addresses automatically.

Slack/Teams notifications:
Add webhook integration to alert security teams when compromised accounts are detected.

Password strength scoring:
Combine HIBP checking with zxcvbn or similar libraries to provide holistic password strength assessment.

MFA enforcement triggers:
Automatically enforce MFA for accounts found in breaches as part of your identity management workflow.

Security Considerations

Never log passwords: The script outputs [REDACTED] instead of the actual password in reports. Maintain this practice.

API key protection: Store your HIBP API key in environment variables, not hardcoded in scripts:

export HIBP_API_KEY="your-key-here"
python hibp_auditor.py --email test@example.com --api-key $HIBP_API_KEY
Enter fullscreen mode Exit fullscreen mode

TLS verification: The requests library verifies TLS certificates by default. Don't disable this.

Rate limits: Respect HIBP's rate limits. They provide this service for free — don't abuse it.

GitHub Repository

The complete script, requirements.txt, and this tutorial are available on GitHub:

GitHub logo ShadowStrike-CTF / hibp-breach-auditor

A Python CLI tool to check email addresses against the HaveIBeenPwned breach database.

hibp-breach-auditor

A Python CLI tool to check email addresses against the HaveIBeenPwned breach database.






Conclusion

Password reuse and credential stuffing remain among the most effective attack vectors in 2026. Building a simple Python CLI tool that checks against HaveIBeenPwned's database gives you a practical way to assess exposure for individuals or small teams.

The k-anonymity implementation means you can check passwords safely without ever transmitting them, and the email breach checking provides immediate visibility into which accounts need attention.

For security teams, incident responders, and IT administrators working with small-to-medium organisations, this is a foundational tool that costs nothing to run and provides immediate actionable intelligence.


Built by ShadowStrike (Strategos) — where we build actual security tools instead of theatre 🎃.

Top comments (0)