Security Basics
6 min
A customer writes to say they can no longer access their account. Their password has changed, invoices are appearing in the wrong place, and another user may have exported their data. What causes account takeover is rarely one dramatic exploit. More often, it is a small weakness in login, session handling, account recovery, or access control that turns a valid user identity into an attacker’s foothold.
For a SaaS founder, the distinction matters. A stolen password is an incident. An application that lets an attacker keep control, bypass checks, or reach other accounts is a product security failure. The first may begin outside your app. The second is squarely within your ability to find and fix.
What causes account takeover in web applications?
Account takeover happens when an attacker can act as a legitimate user, or convince your application that they are one. That can mean logging in with stolen credentials, stealing an active session, abusing a password-reset flow, forging a token, or exploiting an authorisation flaw after signing in as a low-privilege user.
The common thread is identity. Your application needs to answer two separate questions on every sensitive request: who is making this request, and are they allowed to do this specific thing? Authentication answers the first. Authorisation answers the second. Fast-moving products often get the login screen working, then assume the rest follows automatically. It does not.
AI-assisted development can make this worse in a very specific way. Generated code can produce convincing authentication middleware, token parsing, and database queries that appear complete but omit an ownership check, trust a client-supplied user ID, or use an overly powerful server credential. The application works during normal testing because everyone is using their own account. It fails when someone deliberately changes an ID, replays a request, or supplies a token the system should reject.
Stolen credentials are only the starting point
Credential stuffing remains one of the simplest routes into an account. Attackers take email and password combinations exposed by another breach and try them against your login endpoint. They do not need to guess passwords if users have reused them.
Phishing and malicious browser extensions can produce the same result. So can exposed test credentials in a public repository, a shared screen recording, or a support conversation where a temporary password was handled carelessly. A secure password hash does not prevent takeover when the attacker already has the correct password.
Your app should therefore limit repeated login attempts, watch for unusual bursts of failed sign-ins, and support multi-factor authentication where the risk warrants it. But rate limiting and MFA are not a substitute for correct application controls. If a user’s account is compromised, the attacker should still be unable to alter billing, add a new recovery method, export sensitive records, or access another customer’s data without further verification.
There is a trade-off. Extra challenges at every login can create friction for a low-risk consumer product. For admin accounts, financial actions, bulk exports, password changes, and changes to payout details, friction is usually cheaper than fraud. Step-up verification for those actions is often a sensible middle ground.
Session theft turns a browser into a master key
After login, most applications issue a session cookie or JWT. That token is effectively proof that the user has authenticated. Whoever holds it may not need the password at all.
Account takeover can follow when session tokens are exposed in frontend storage, sent over an insecure connection, logged by a monitoring tool, or leaked through a cross-site scripting vulnerability. If a malicious script can run in your app’s origin, it may read tokens stored in local storage, make requests as the user, or alter what the user sees.
Cookie configuration matters here. Sensitive session cookies should normally be Secure, HttpOnly, and protected with an appropriate SameSite setting. Secure requires HTTPS. HttpOnly reduces the chance that injected JavaScript can read the cookie. SameSite helps constrain cross-site request behaviour, though it must be chosen carefully if your login or payment flow legitimately crosses domains.
JWT implementations deserve close review too. Common findings include accepting unsigned JWTs, failing to validate the issuer or audience, using weak or exposed signing secrets, and trusting claims supplied by the client. A token that decodes successfully is not necessarily a token your app should accept. Signature, expiry, issuer, audience, and intended use all need validation.
Session lifecycle is easy to overlook. A password reset should invalidate existing sessions. So should a suspected compromise, and often a material account change such as a new recovery email. Without revocation or versioning, an attacker with an old session can remain signed in after the victim believes the problem is solved.
Broken authorisation can look exactly like takeover
Some of the most damaging incidents do not require an attacker to defeat login at all. They register their own account, inspect a request, change a record ID, and gain access to another user’s resource. This is commonly called insecure direct object reference, or IDOR.
Imagine an endpoint that accepts GET /api/invoices/1234. If the server checks only whether the requester is logged in, rather than whether invoice 1234 belongs to that requester or their organisation, an attacker can enumerate invoice IDs. The same pattern affects profile updates, file downloads, support tickets, team invitations, saved payment methods, and admin functions.
This is still account takeover in business terms when it lets an attacker change another user’s email address, reset their password, create an API key, or modify permissions. The application has allowed one account to assume control over another account’s security boundary.
The fix is not hiding IDs or switching from integers to UUIDs. Unpredictable identifiers reduce casual guessing but do not enforce access. Every server-side action must derive the authenticated identity from a trusted session or token and verify ownership or role against the resource being requested.
Supabase and backend configuration mistakes create shortcuts
Supabase can speed up a product dramatically, but its row-level security settings must match the application’s tenancy model. A table with RLS disabled, an overly broad policy, or a policy based on client-controlled values can expose user records directly through the API.
A frequent high-severity issue is an exposed service-role key in a frontend bundle, repository, or environment configuration that reaches the browser. Service-role keys bypass RLS. If one is public, an attacker may be able to query or modify data across the project, depending on how the backend is configured. Rotating the key is urgent, but it is only the first task. You also need to remove it from the client, inspect logs, verify RLS on affected tables, and determine whether the secret reached source control history or a deployed build.
Backend APIs can introduce the same problem outside Supabase. A route that accepts a userId from the request body and runs a privileged database query may work perfectly until someone changes that value. Server-side credentials should be used only where required, kept out of client code, and paired with explicit authorisation checks.
Account recovery is an attacker’s preferred route
Password reset and email-change flows are security-critical features, not routine forms. If an attacker can obtain, predict, replay, or intercept a recovery token, they may not need the victim’s password at all.
Review whether reset tokens are random, single-use, short-lived, and invalidated once used. They should be stored safely, not exposed in application logs, and tied to the intended account. The reset endpoint should not reveal whether an email address exists, as that helps attackers build a target list.
Email-change flows need similar care. Requiring only an active session can be insufficient when a session may have been stolen. Re-authentication, confirmation to the old address, confirmation to the new address, and alerts about the change each reduce risk. The right combination depends on the value of the account and the harm a fraudulent change could cause.
Also inspect team and organisation invitations. An invitation accepted by the wrong email, a token that never expires, or a role chosen by the invite recipient can hand over access without touching the primary login flow.
Find the chain, not just the headline issue
A takeover is often a chain of ordinary defects: a leaked API key, permissive database policy, IDOR in an account settings endpoint, and no alert when the recovery email changes. Fixing only the first finding may leave the attacker another route back in.
That is why a useful application review tests the live app and the repository together. Automated checks can identify exposed credentials, outdated dependencies, risky headers, and common patterns. Manual testing then establishes whether a real user can cross a boundary: alter another account’s data, reuse a reset token, bypass an RLS policy, or maintain a session after a password change.
The result should be prioritised by what an attacker can actually do. A missing content security policy deserves attention, but a public service-role key or a working IDOR that exposes customer records should be treated as an immediate operational issue. HollowByte assesses these paths across the deployed application and codebase, then maps each finding to the exact control that needs changing.
If your product has grown quickly, start with the routes that can change identity, money, permissions, and data access. Test them as an attacker would, not just as the happy-path user you built them for. That is where account takeover risk becomes visible early enough to fix it.
Back to blog
