How Holehe's Password Recovery Detection Method Works: A Technical Deep Dive

Holehe detects accounts by programmatically invoking services' "forgot password" flows, analyzing responses for error codes or masked recovery data that reveal whether an email is registered—all without authenticating.

The open-source email checker Holehe categorizes its detection techniques by strategy. The password recovery detection method stands out as a non-intrusive approach that leverages public password-reset endpoints to verify account existence. This article explains how Holehe implements this method, from module selection to response parsing, based on the megadose/holehe source code.

How Holehe Selects Password Recovery Modules

Holehe's modular architecture loads and filters detection strategies dynamically. The framework handles password-recovery modules through a specific pipeline in holehe/core.py.

Module Discovery and Filtering

When you run Holehe, the core system executes three key steps:

  1. Load all modules using import_submodules("holehe.modules") — this recursively imports every Python file in the modules directory
  2. Extract coroutine functions via get_functions() which scans imported modules for async functions matching the required signature
  3. Filter by method type — unless you pass --no-password-recovery, modules with method = "password recovery" are included in the scan

This filtering logic appears around line 8 of the core execution flow, where the code checks each module's method attribute against the user's CLI flags.

The Async Execution Model

Selected modules run as concurrent async tasks. The launch_module() coroutine spawns each check with three standard arguments:

await module_function(email, client, out)
  • email: The target email address to investigate
  • client: A shared httpx.AsyncClient instance for connection pooling
  • out: A list that collects result dictionaries from all modules

This design allows dozens of password-recovery checks to run in parallel without blocking.

Anatomy of a Password Recovery Module

Every password-recovery module in Holehe follows an identical five-phase pattern. Examining holehe/modules/software/adobe.py, holehe/modules/social_media/odnoklassniki.py, and holehe/modules/products/samsung.py reveals this structure clearly.

Phase 1: Request Construction

Modules build HTTP requests that mimic legitimate browser behavior:


# From adobe.py (lines 21-27)

headers = {
    "User-Agent": random.choice(ua),
    "Accept": "application/json",
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest",
    "Origin": "https://auth.services.adobe.com",
    "Referer": "https://auth.services.adobe.com/"
}

Key elements include:

  • Rotating User-Agent strings from holehe/localuseragent.py
  • CSRF tokens scraped from initial page loads when required
  • Cookies gathered from session initialization requests
  • Realistic headers matching the service's expected client fingerprint

Samsung's module (samsung.py) demonstrates advanced preparation: it first fetches a CSRF token from an endpoint, then includes it in subsequent password-recovery requests (lines 28-35).

Phase 2: Recovery Request Execution

Most modules send POST requests to password-reset endpoints. Some require multi-step flows:

Service Request Pattern Endpoint Target
Adobe Single POST /ims/account/v3/password/reset
Odnoklassniki GET → GET Login page → Recovery page
Samsung POST with CSRF /v1/otp via Samsung Account API

The Odnoklassniki implementation (odnoklassniki.py, lines 24-26) illustrates a two-stage approach:


# First: establish session context

await client.get("https://ok.ru/dk?st.cmd=anonymLogin")

# Then: request recovery form

await client.get("https://ok.ru/dk?st.cmd=anonymPasswordRecovery")

Phase 3: Response Interpretation

This critical phase determines account existence through response analysis. Holehe modules check multiple signal types:

JSON API responses (Adobe pattern, lines 30-38):

j = r.json()
if "errorCode" in str(j.keys()):
    # Error codes indicate unknown email

    exists = False
else:
    # Valid response structure = registered account

    exists = True

HTML parsing (Odnoklassniki pattern, lines 39-42):

soup = BeautifulSoup(response.text, "html.parser")
masked_email_element = soup.find('button', {'data-value': True})
if masked_email_element:
    masked_email = masked_email_element.get_text()

Status code heuristics: HTTP 404 or specific redirect patterns often indicate unregistered emails.

Phase 4: Recovery Data Extraction

When services expose masked recovery information, modules capture it in standardized fields:

Field Content Type Example Source
emailrecovery Masked secondary email Adobe's secondaryEmail JSON key (line 59)
phoneNumber Masked phone number Samsung's get_phone_number() result (lines 49-55)
others Additional metadata Provider-specific hints

Adobe's extraction (lines 55-62):

if "secondaryEmail" in response:
    emailrecovery = response["secondaryEmail"]
else:
    emailrecovery = None

Phase 5: Result Aggregation

Every module appends a standardized dictionary to the shared out list:

out.append({
    "name": "adobe",
    "domain": "adobe.com",
    "method": "password recovery",
    "rateLimit": rate_limit_flag,
    "exists": exists_boolean,
    "emailrecovery": recovered_email,
    "phoneNumber": recovered_phone,
    "others": additional_data
})

Why Password Recovery Detection Works

The method exploits a fundamental design tension in identity systems: password-reset endpoints must verify identity without authentication, making them informationally leaky by necessity.

Services face a UX dilemma when handling forgotten passwords:

  • Return "user not found" → leaks that email is unregistered
  • Return "check your email" → confirms registration but hides state
  • Expose masked recovery options → confirms registration + reveals partial data

Holehe capitalizes on services choosing the second or third option. The method requires zero credentials, generates minimal server load, and bypasses services that deliberately obscure registration status on profile/login pages.

Running Password Recovery Checks

Default Execution (Includes Password Recovery)

holehe target@example.com

Output format shows recovered data:


[+] adobe.com      secondaryEmail@example.com    / 1234**56**78
[+] ok.ru          masked_email@example.com      / +33 6 ** ** 78
[+] samsung.com    N/A                           / +1 ***-***-7890

Excluding Password Recovery for Speed

holehe target@example.com --no-password-recovery

This filters out all modules where method == "password recovery", reducing the module set to registration-based and login-based detection only.

Programmatic Usage

import asyncio
import httpx
import holehe.core as hh

async def check_with_recovery(email: str):
    """Run Holehe including password-recovery modules."""
    client = httpx.AsyncClient(timeout=10)
    out = []
    
    # Load and filter modules

    modules = hh.import_submodules("holehe.modules")
    websites = hh.get_functions(modules)  # password recovery included by default

    
    # Execute all checks concurrently

    await asyncio.gather(*[
        mod(email, client, out) for mod in websites
    ])
    
    await client.aclose()
    return out

# Run and filter results

results = asyncio.run(check_with_recovery("user@example.com"))
recovery_hits = [r for r in results if r["method"] == "password recovery" and r["exists"]]

Key Implementation Files

File Purpose
holehe/core.py Orchestrates module loading, CLI parsing, and async execution
holehe/modules/software/adobe.py JSON API-based password recovery for Adobe ID
holehe/modules/social_media/odnoklassniki.py HTML-form-based recovery with masked email extraction
holehe/modules/products/samsung.py CSRF-protected recovery with phone number masking
holehe/localuseragent.py Rotating User-Agent pool for request fingerprinting
holehe/instruments.py Progress bar and CLI output formatting

Summary

Holehe's password recovery detection method operates through these core mechanisms:

  • Module flagging: Services set method = "password recovery" to opt into the strategy
  • CLI control: The --no-password-recovery flag filters these modules on demand
  • Request simulation: Each module replicates legitimate forgot-password flows with proper headers, cookies, and CSRF handling
  • Response classification: HTTP status codes, JSON error fields, and HTML structure reveal account existence
  • Data extraction: Masked emails and phone numbers are parsed from successful recovery page responses
  • Rate limit awareness: HTTP 429 responses and exceptions set rateLimit=True to signal temporary blocking

Frequently Asked Questions

Does password recovery detection trigger account security alerts?

Generally no. Password recovery requests are indistinguishable from legitimate user actions. However, aggressive scanning from a single IP may trigger rate limiting (rateLimit=True in results) or temporary blocks. Space requests appropriately and respect services' terms of service.

What information can password recovery modules extract?

Modules capture whatever the service voluntarily exposes during the reset flow: masked email addresses (e.g., t***@example.com), masked phone numbers (e.g., +1 ***-***-7890), and occasionally partial names or account creation dates. They never obtain full passwords or unmasked credentials.

Why do some services use GET requests for password recovery?

Some platforms (like Odnoklassniki) implement password recovery as multi-page flows where the initial request loads a form with session cookies, and subsequent requests submit data. The GET requests establish session context before the actual recovery attempt.

How does Holehe handle services that always return "email sent" regardless of registration status?

These services are unsuitable for password-recovery detection and typically use alternative methods in Holehe (registration-based or login-based checks). Modules only implement password recovery when the response semantics reliably distinguish registered from unregistered emails.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →