Someone is brute forcing logins on your app: how to stop it
Turn on a rate limit on the login endpoint now, add CAPTCHA, and then read your auth logs to see which accounts the attacker tried. A brute force attack sends many password guesses to your login form. The attacker hopes that one guess is correct. Your app must slow the sender down after a few failures.
This guide shows how to confirm the attack, how to stop it, and how to check whether any account is lost. It also shows how to make the login endpoint safe in the long term.
What does “someone is brute forcing logins on my app” mean?
A brute force attack is a series of login attempts with many different passwords for the same account. A script sends the attempts. The script can send hundreds of requests in a minute.
The attack has three goals:
- The attacker wants to guess the password of one chosen account, such as an admin.
- The attacker wants to find any account with a weak password, such as
123456. - The attacker wants to find out which email addresses have an account in your app.
Vibe coding tools often leave the login endpoint without limits. The tool writes a form that calls the auth API. The tool does not add a delay, a counter, or a bot check. The endpoint is public by design, so anyone can call it as often as they want.
Brute force is not the same as credential stuffing. In a brute force attack, the attacker guesses passwords. In credential stuffing, the attacker uses real passwords from breaches of other sites. The first signs look alike. If each attempt uses a different email, suspect credential stuffing.
How do I confirm that the attack is real?
Look for these signs in your logs and your dashboards:
| Sign | Where to look |
|---|---|
| A sudden rise in failed logins | Auth logs of your provider |
| Many attempts for one email address | Auth logs, grouped by email |
| Many attempts from one IP address | Host logs or the log of the auth provider |
| Attempts at a steady, fast rate | The time stamps. A human cannot send ten logins in a second. |
A rise in 429 responses |
Your host or API gateway logs |
| A rise in the bill or in the load | Hosting dashboard, database load |
| A user reports a “new login” email that they did not trigger | Support inbox |
Then test your own endpoint. Use a test account that you own. Send 200 wrong passwords in a row and see what the endpoint does:
for i in $(seq 1 200); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
"https://YOUR-PROJECT.supabase.co/auth/v1/token?grant_type=password" \
-H "apikey: YOUR_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"wrong-'$i'"}'
done
Replace the URL with the login endpoint of your own app. If you use another backend, use its login URL and body. If no response is 429, the endpoint has no effective limit for this test. The default Supabase limit of 150 requests per 5 minutes should stop some of the 200 requests.
Warning: Run this test only against your own app and your own test account. Do not run it against another service.
What do I do first? Fix it now, in order of urgency
The steps below go from the fastest to the slowest.
- Block the active source. If the attack comes from a few IP addresses, block them in your host, your CDN, or your firewall. This step is a short-term help only. Attackers change addresses.
- Turn on a rate limit at the edge. Most hosts and CDNs offer a rule such as “limit requests to
/loginper IP address”. Set a low limit, for example 10 requests per minute. - Check the limits of your auth provider. Supabase limits some endpoints per IP address by default. See the next section.
- Turn on CAPTCHA on the login form. See the section below.
- Add a limit per account. Count the failed logins for each email address. Delay or block the next attempt after a set number.
- Force a password reset for accounts that the attacker hit. Use the damage checks to find them.
- Turn on MFA for admin accounts. MFA makes a guessed password worthless on its own.
- Use the same error message for all failures. Do not tell the caller whether the email exists.
Caution: Do not lock an account for a long time after a few failures. An attacker can use the lock to stop a real user from signing in. Use a short and growing delay instead.
What does Supabase already limit?
The Supabase rate limits page lists the defaults. The values that matter for this attack are:
| Endpoint | Limited by | Default |
|---|---|---|
/token (password, refresh token, ID token, PKCE) |
IP address | 150 requests per 5 minutes, bursts up to 30 |
/signup, /recover, /resend, /magiclink, /otp, /user |
IP address | 30 requests per 5 minutes |
/verify |
IP address | 30 requests per 5 minutes |
| MFA challenge and verify | IP address | 15 requests per minute |
| Password reset per user | User | One request per 60 seconds |
The password sign-in goes through /token. The limit is 150 requests per 5 minutes per IP address. That is a high number for guessing passwords. An attacker can try about 30 passwords per minute from one address and stay inside the limit. Lower the value in the dashboard if your project allows it. Open Authentication, then Rate Limits.
Supabase limits by IP address. The limit is not per account. An attacker with many addresses can aim at one account and stay under the limit on each address. Add a limit per account in your own layer, as the next sections show.
Note: If your server calls Supabase on behalf of the user, the limit can apply to your server address and not to the user address. The same page explains that you can send the end user address in the Sb-Forwarded-For header. It requires a secret API key, and a new project must turn on the IP Address Forwarding setting first. Read the page before you change this.
How do I add CAPTCHA to the login form?
CAPTCHA asks the sender to solve a task that a script cannot solve easily. The Supabase CAPTCHA guide lists two providers: hCaptcha and Cloudflare Turnstile. It protects the sign-in, sign-up, and password reset forms.
To turn it on, follow these steps:
- Create a site in the dashboard of your CAPTCHA provider. Copy the site key and the secret key.
- Open the Supabase dashboard. Go to Authentication, then the bot and abuse protection settings.
- Turn on Enable CAPTCHA protection.
- Select the provider and enter the secret key. Save the settings.
- Add the widget to your login form with the site key.
- Send the token with the login request.
The guide shows the token in the options of the signUp call. Use the same options.captchaToken field for the sign-in call, and check the Supabase JavaScript reference for the exact name:
await supabase.auth.signInWithPassword({
email,
password,
options: { captchaToken },
})
Keep the CAPTCHA secret key in the dashboard of your auth provider. Do not put it into frontend code. See environment variables security.
CAPTCHA is one layer. The OWASP Authentication Cheat Sheet calls it a defense-in-depth control and not a complete prevention. It also says that a CAPTCHA that appears only after a few failed attempts is easier for real users.
How do I add a limit per account?
An IP limit alone fails against many addresses. Count failures for each account. The OWASP cheat sheet gives this advice: track failed logins per account and not per IP address, so that rotating addresses does not bypass the limit. It also says that you must define the threshold, the observation window, and the lockout duration.
For a custom backend, use a small table:
create table public.login_failures (
email text not null,
created_at timestamptz not null default now()
);
create index on public.login_failures (email, created_at);
alter table public.login_failures enable row level security;
The table has no policy, so the browser cannot read or write it. Only your server code with a secret key can use it. Before each login, count the rows for the email in the last 15 minutes. If the count is 5 or more, return the same error that a wrong password returns, and wait before the next check. Delete the rows after a successful login.
Use a growing delay instead of a permanent lock. The OWASP guide describes exponential delay, where each failure doubles the wait time, and it advises that you allow password recovery while an account is delayed. This stops an attacker from using the lock against real users.
Why must all login errors look the same?
Different messages tell the attacker which emails have accounts. “No user with this email” and “Wrong password” are two such messages. The OWASP guide says that login, reset, and recovery must give the same generic response for all failures. It also says that the response time and the HTTP status code must not differ.
Use one message for all cases, for example: “Login failed. Invalid email or password.”
How do I check the damage?
You must find out whether the attacker guessed any password. Look for a successful login that follows many failures.
- List the targeted accounts. In the auth logs, group failed logins by email address. Note each email with more than five failures.
- Find successes after failures. For each targeted email, check whether a successful login follows the failures. A success after a long series of failures is a strong sign of a guessed password.
- Check the source. Compare the IP address and the country of the success with the earlier sign-ins of that user.
- Check the account actions. Read what the account did after the login. Look for data exports, password changes, email changes, API key creation, and payment changes.
- Check admin accounts first. An admin account gives the attacker the most power. Review the role changes and the user list exports.
- Check weak passwords. If your app has a leaked password check or a minimum length, make sure that it is on. If not, assume that users with short passwords are at risk.
- Read your host logs. Look at the volume of requests to the login path. Find the start time of the attack.
If you find a successful login that looks wrong, treat the account as taken over. Sign out all sessions, reset the password, and check the email address of the account. See the authentication implementation guide for a safe baseline.
Note: Auth logs in the dashboard have a limited retention time that depends on your plan. Export the logs now if you need them as evidence.
How do I stop the next attack?
Use these controls as a permanent baseline.
- Limit the login endpoint per IP address and per account. Use both limits, because each one covers the gap of the other.
- Use CAPTCHA with care. Show it after the first failures, or always on the login form.
- Require MFA for admin accounts. The OWASP guide calls MFA the best defense against most password attacks.
- Set a minimum password length. See the Supabase weak password error for how the setting behaves.
- Alert on failed logins. Send a message to yourself when failed logins pass a set number in 5 minutes.
- Alert on new sign-ins. Send an email to the user when a login comes from a new device or place.
- Log each failed login. Store the time, the email, and the IP address. Keep the logs long enough to find an attack that is slow.
- Scan the app after each release. AI tools can change the auth code later and remove a limit that worked before.
For more patterns on limits for your public endpoints, read API abuse protection.
When should I get help?
Get help in these cases:
- You find a successful login after many failures on an admin account.
- The attack continues after you set limits and CAPTCHA, and the load harms your app.
- The attack comes from thousands of different IP addresses. You may need a CDN or a bot protection service.
- The attacker could read personal data or payment data. You may have a duty to report a breach.
- You cannot tell from your logs which accounts the attacker tried. Ask a security engineer to review the setup.
After you apply the fixes, repeat the 200-request test. The endpoint must now return 429 or a delay after a small number of failures.
Common questions
How do I know that the attack is a brute force attack?
Does a rate limit per IP address stop the attack?
Can the lockout of an account harm my real users?
Is CAPTCHA enough?
Do strong passwords stop brute force?
Test your login limits
A VibeEval scan sends repeated requests to the login endpoints of your deployed app and reports when the app does not slow down or block the sender.
14-day free trial · No credit card · Cancel anytime