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:
- Load all modules using
import_submodules("holehe.modules")— this recursively imports every Python file in the modules directory - Extract coroutine functions via
get_functions()which scans imported modules for async functions matching the required signature - Filter by method type — unless you pass
--no-password-recovery, modules withmethod = "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 investigateclient: A sharedhttpx.AsyncClientinstance for connection poolingout: 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-recoveryflag 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=Trueto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →