In this blog post, we study the Spotify-Account-Checker open source project. The author describes it as:
“An automated tool for checking the validity of Spotify accounts with proxy support, multi-threading capabilities, and Discord Rich Presence integration.”
At first glance, checking whether an account exists may look harmless. It does not authenticate users and it does not modify any data. However. from a security standpoint, this is not a neutral signal.
Leaking whether an account exists is a well-known weakness called account enumeration. OWASP explicitly documents that attackers use these signals to collect valid usernames or emails, which makes brute force and credential stuffing attacks more efficient.
We investigate how the account checker operates, the techniques used to scale the enumeration, and what countermeasures you should implement to protect against account enumeration.
TL;DR
- We study Spotify-Account-Checker, an open source project that automates checking whether an email is registered on Spotify.
- The tool relies on a public signup API endpoint that returns different responses depending on whether an account exists (
status: 20) or not (status: 1). - Under the hood, it uses a simple
requestsbased HTTP client, proxy support, and multi-threading viaThreadPoolExecutor. - The
check_accountfunction stores both email and password combinations, even though only the email is validated via the signup endpoint. - No advanced evasion techniques are used. The effectiveness comes from the API leaking account existence.
- We explain why this behavior conflicts with OWASP guidance and provide concrete mitigation recommendations.
How enumeration is used in real attack chains
Knowing that an email is registered is typically an intermediate step in a broader workflow.
- Improving credential stuffing efficiencyIf an attacker confirms that an email is registered, they can test passwords leaked from other breaches for that specific account. This increases their success rate and reduces unnecessary traffic.
- Targeted phishingA curated list of confirmed accounts allows attackers to craft more credible phishing messages and avoid sending campaigns to non-users.
- Data collection and automation supportEnumeration endpoints can be used to measure user presence for specific domains or regions and to enrich datasets used in other automated abuse workflows.
This pattern is explicitly described in OWASP’s Web Security Testing Guide: collect valid identifiers first, then use them to optimize further attacks.
Project overview: simple tooling, high-signal API
At a high level, the project:
- Takes a list of email/password pairs.
- Sends requests to a Spotify signup validation endpoint.
- Uses proxies to distribute traffic.
- Uses multi-threading to increase throughput.
The important point is technical and straightforward:
The tool does not rely on browser automation, TLS fingerprint spoofing, or advanced HTTP impersonation. It uses a standard Python HTTP client. Its effectiveness comes from the API returning a clear, machine-readable account existence signal.
When an endpoint leaks high-confidence information, even basic automation is sufficient to extract value.
The signup endpoint used for enumeration
The checker relies on the following endpoint:
https[:]//spclient[.]wg[.]spotify[.]com/signup/public/v1/account?email=test@example.com&key=REDACTED&validate=1
Depending on whether the email is already registered, the JSON response differs.
Example: existing email
{
"status":20,
"errors": {
"email":"That email is already registered to an account."
},
"country":"FR",
"minimum_age":15
}
Example: non-existing email
{
"status":1,
"country":"FR",
"minimum_age":15
}
The difference between status: 20 and status: 1, combined with the explicit error message, provides a reliable enumeration oracle.
This is precisely the type of differential feedback OWASP recommends minimizing.
Code analysis: the AccountChecker class
The core logic is implemented in the AccountChecker class:
The check() method sends a GET request and interprets the JSON response:
def check(self, email: str) -> bool:
response = self.session.get(f'<https://spclient.wg.spotify.com/signup/public/v1/account?email={email}&key=bff58e9698f40080ec4f9ad97a2f21e0&validate=1>')
if response.status_code == 200:
if response.json()['status'] == 20:
return True
else:
return False
else:
log.failure(f"Failed to check {email[8]}... : {response.text}, {response.status_code}")
return None
The logic is:
- HTTP 200 response.
- Parse JSON.
- If
status == 20, the email is considered registered.
There is no ambiguity in how the signal is extracted.
A noteworthy detail: storing email and password pairs
The check_account function takes both email and password as input:
def check_account(email: str, password: str, Misc: Miscellaneous) -> bool:
max_retries = config['dev'].get('MaxRetries', 3)
retry_delay = 1
account_line = f"{email}:{password}"
for attempt in range(max_retries):
try:
proxies = Misc.get_proxies()
Account_Checker = AccountChecker(proxies)
verified = Account_Checker.check(email)
if verified is not None:
if verified:
with Misc.valid_file_lock:
with open("output/valid.txt", "a") as f:
f.write(f"{account_line}\\n")
f.flush()
log.success(f"Valid Account: {email[:8]}... | {password[:8]}...")
# ...
Only the email is used to query the signup endpoint. The password is not validated through this API.
However, when the account is marked as valid, the script stores the full email:password pair in output/valid.txt.
The repository does not implement a login phase. But structurally, this pattern mirrors a common attack workflow:
- Validate that the account exists.
- Keep the associated email:password pair.
- Use the filtered list for further authentication attempts.
Pre-validating account existence before attempting login reduces noise, avoids unnecessary authentication traffic, and increases the efficiency of credential stuffing campaigns.
The goal here is not to attribute intent to the author of the project. Rather, this detail illustrates a broader point: account enumeration is frequently an initial step in multi-stage attack chains. Even if the enumeration logic appears isolated, its output can be directly reused in downstream abuse workflows.
HTTP client characteristics
The tool uses Python’s requests library with a standard Session:
def __init__(self, proxy_dict: str = None):
self.session = requests.Session()
self.session.headers = {
'accept': '*/*',
'accept-encoding': 'gzip, deflate, br',
}
self.session.proxies = proxy_dict
Technical observations:
- It uses a default
requests.Session()with minimal custom headers. - Proxy routing is configured via
session.proxies. - It does not attempt to replicate a full browser header set.
- It does not implement TLS fingerprint spoofing or low-level protocol manipulation.
In other words, this is standard HTTP automation. The barrier to entry is low because the endpoint itself provides a clean account existence signal.
Scaling enumeration with concurrency
The script increases throughput using ThreadPoolExecutor:
with ThreadPoolExecutor(max_workers=thread_count) as executor:
futures = []
for email, password in accounts: # Now we can safely unpack
futures.append(executor.submit(check_account, email, password, Misc))
for future in as_completed(futures):
try:
if future.result():
total = title_updater.increment_total()
status.total_accounts = total
except Exception as e:
log.failure(f"Thread error: {e}")
This transforms a single validation request into bulk enumeration by running multiple checks in parallel.
From a defender perspective, this matters because enumeration attempts can be distributed across IPs and executed concurrently, making naive rate limits insufficient.
Mitigations and defensive considerations
1. Minimize explicit account existence signals
If your API returns materially different responses for existing versus non-existing accounts, assume it can be automated.
Options include:
- Generic responses.
- Delayed or indirect feedback mechanisms.
- Additional verification steps before confirming account status.
2. Treat pre-auth endpoints as high-risk surfaces
Signup validation, forgot-password, and login endpoints are often the first step in account takeover workflows. They should be protected accordingly.
3. Add bot detection and abuse controls
Even if an endpoint does not grant access, it can enable attack optimization.
Defensive measures may include:
- Multi-dimensional rate limiting.
- Behavioral detection across IP, device, and identifier dimensions.
- Progressive friction when automation patterns are detected.
4. Enforce stronger client integrity where appropriate
If an endpoint is intended only for first-party clients, consider requiring additional proof of client legitimacy:
- Short-lived server-issued tokens.
- Request signing.
- Integrity checks tied to a validated session.
- Mobile app attestation signals.
These mechanisms can be reverse engineered, but they increase attacker cost and reduce trivial automation.
Final thoughts
This project illustrates a recurring pattern in abuse workflows:
- Attackers do not need advanced evasion techniques if the application provides high-confidence signals.
- Account enumeration is rarely the end objective. It is an enabling step that increases the efficiency of credential stuffing, phishing, and broader abuse operations.
- If you expose account existence signals, assume they will be collected and used at scale.