Fraud playbook

How to stop account takeover

Alex MugoFounder, Kaidn
4 min readRevised

Also called: ATO · credential stuffing · account hijacking · session hijacking · login fraud

Why account takeover is caught by what changed rather than what is wrong, how credential stuffing is actually run, and where detection stops and authentication has to take over.

What it costs you

  • trending_downWhatever the account holds: stored balance, loyalty points, saved payment methods, and any payout path the real owner set up and you now honour for somebody else.
  • trending_downThe refund, almost always. When a customer is compromised on your service you generally make them whole regardless of whose password it was.
  • trending_downTrust, which does not recover at the rate the balance does. Users who have been taken over on your platform tell people.
  • trending_downSupport load at its most expensive: identity re-verification, manual recovery, and a customer who is upset and correct to be.
  • trending_downRegulatory exposure where personal data was exposed in the compromised account, which is a different and slower kind of cost.

How the attack runs

  1. 01

    Obtain credentials somebody else leaked

    Almost never from you. Combolists from other services' breaches are cheap and enormous, and they work because password reuse is close to universal. The attacker is betting on your users having an account elsewhere that fell over.

  2. 02

    Test the list against your login

    Automated, distributed and slow enough per address to stay under naive limits. Success rates in the low single digits per cent are entirely worth it at list scale.

  3. 03

    Confirm and quietly sit

    A careful operator does not act immediately. They confirm the login works, take note, and return later, because instant action is what triggers alerting and what a victim notices.

  4. 04

    Change the recovery path

    The step that turns a login into ownership: swap the email, add a phone, remove a passkey, register a new device. Once recovery is theirs, resetting the password does not evict them.

  5. 05

    Extract, usually through a route you allow

    Withdrawal to a new destination, a gift card purchase, a points transfer, an in-product payout. Rarely anything exotic, because your own legitimate features are the cleanest way out.

What does not work

These are the defences most teams try first. They are listed here because trying them and watching them fail is expensive.

Locking an account after N failed passwords

It converts a takeover attempt into a denial-of-service against your own customers. An attacker with a list can lock thousands of real users out on purpose, and your support queue becomes the attack. Throttle and score instead of locking.

Forcing regular password rotation

Long since abandoned by every serious guidance body. It pushes people toward predictable variations of a password they already reuse, which makes the combolist problem worse, not better.

SMS one-time codes as the main defence

Genuinely better than nothing and the weakest of the second factors. SIM swap and interception are real, and the codes are phishable in real time by a page that simply asks for them. Treat it as a step up, not as the answer.

Blocking VPN and proxy logins

A meaningful share of privacy-conscious, travelling and corporate users are permanently behind one. As a hard rule this locks out real customers daily to inconvenience an attacker who will use a residential proxy anyway.

Emailing 'a new device signed in'

Useful and not a control, because it is a notification rather than a decision. If the attacker changed the email first, you have just told them. Notify, but do not let a notification stand in for a challenge.

The signals that do

These are the signals that carry weight on account takeover, and why each one is the signal rather than the obvious alternative. Not all of them are ours: the ones marked you build this genuinely work and Kaidn does not check them, so you would be wiring them up yourself. Listing those unlabelled would read as a claim we cannot support.

Deviation from this account's own history

you build this

The defining signal of the whole category, and what makes it structurally different from signup fraud. There is a real user with real history here, so you are not asking whether this looks suspicious in general, you are asking whether it looks like them. A new device, a new ASN, a new country and an unusual hour together are a strong break.

Credential-stuffing shape across accounts

Kaidn checks this

One failed login is noise. Many accounts each seeing a small number of failures from a shared device, ASN or timing rhythm is a list being worked. That pattern is invisible per account and obvious across them.

Impossible travel

Kaidn checks this

Crude, and still useful when treated as evidence rather than proof. Two successful logins from locations that cannot both be true in the elapsed time deserve a challenge. It is noisy with VPNs, so it belongs in a score with a weight rather than as a hard rule.

Post-login sensitive actions

Kaidn checks this

The login itself is often unremarkable and the change of email, phone or payout destination is not. Scoring those events separately, with the login context attached, catches the cases that got through the door.

Velocity on password reset and recovery

Kaidn checks this

Recovery flows are the softer target and are frequently monitored less than login. Bursts against reset endpoints usually precede the takeovers you will hear about a week later.

Account takeover is the inverse of everything else on this site. The other guides are about accounts that were never real. This one is about accounts that are entirely real, belong to somebody who trusted you, and are now being operated by a stranger.

That inversion changes the detection problem completely, and it is worth being explicit about it, because a tool tuned for one is often poorly suited to the other.

You have history, so use it#

With a fake signup there is nothing to compare against. With takeover there is a real person with months of behaviour behind them, and the question is not "does this look risky" but "does this look like them".

That is why generic risk signals underperform here. A residential IP, an ordinary device and a clean email are all completely normal for an attacker who is using a proxy in the victim's country, and they are also normal for the victim. What separates them is the break from an established pattern: a device never seen on this account, on a network this user has never used, at an hour they have never been awake, going straight to the withdrawal page.

Almost none of this starts with you#

Credential stuffing works on password reuse. The passwords come from somebody else's breach, which means a compromised account is usually not evidence that your security failed.

It is also why "make our passwords stronger" is the wrong lever. Your users' passwords are already strong or weak elsewhere, and you will not fix that with a complexity rule. What you can do is detect the list being worked, which is visible across accounts even when each account sees only two or three failures.

Watch the recovery path harder than the login#

If you only harden one thing, harden this.

Login is where attention goes and where the attacker expects to be watched. The change of email address, phone number, passkey or payout destination is where a session turns into permanent ownership, and it is very often scored less carefully, or not at all.

Score those as their own events, with the login that preceded them attached. A password change from a device first seen four minutes ago is a different proposition from one made on a laptop that has been signing in for two years, and treating both as "settings updated" is how accounts are lost.

Detection has a ceiling here, and authentication is above it#

We will be straight about this, because it matters more on this page than on the others: the strongest control against credential stuffing is not scoring, it is authentication that a stolen password cannot satisfy.

Passkeys end credential stuffing for the accounts that adopt them, in a way no detection layer can match. If you can move users onto them, do that first and treat everything below as coverage for the accounts that have not migrated, the recovery flows, and the phishing cases where the user handed over a live code.

Anyone selling you detection as a replacement for that is selling you the second best option as the first.

Where we sit#

We score login and post-login events with the account's own history as context, plus device, IP and ASN reputation, velocity across accounts, and email identity. Verdicts return the checks that fired and their weights, so a step-up can be triggered on the specific signal you care about.

We do not ship the challenge itself. We return a verdict; issuing the step-up, the passkey prompt or the recovery hold is yours to implement. Tools built specifically around the login interaction, such as Castle, do that part and do it well, and running both is a coherent architecture rather than a hedge.

Score the recovery path as its own event#

The login call is the one everybody writes. This is the one that matters more:

app/api/account/email/route.ts
// a change of recovery address is where a session becomes ownership
const r = await kaidn.score({
  event: "email_change",
  user_id: user.id,
  ip: req.ip,
  email: body.newEmail,
  device_id: body.kaidn_device_id,
});

// the age of the device on THIS account is the discriminator, and it is yours
const deviceIsNew = !(await db.accountDevices.exists({
  user_id: user.id,
  device_id: body.kaidn_device_id,
}));

if (deviceIsNew || r.verdict !== "allow") {
  // do not refuse; delay and notify the address you already had
  await holdChangeFor(user.id, "24h");
  await notifyOldAddress(user, r.event_id);
  return { status: "pending_confirmation" };
}

A 24-hour hold plus a mail to the address being replaced is worth more than any score on this flow, because it gives the real owner the one thing an attacker needs them not to have: time.

The session signals worth routing on#

Session risk is where takeover shows up, and these are the reasons the engine emits for it:

reasoncheckwhat it is saying
impossible_travelsessionRisktwo sessions too far apart to be the same person
device_ip_hoppingsessionRiskone device moving across networks faster than a person moves
ip_cloakingsessionRiskthe network is being hidden mid-session
vpn_dropsessionRiska VPN disconnected and revealed a different origin
velocityvelocityattempts across accounts at a rate no human produces
app/api/login/route.ts
const r = await kaidn.score({ event: "login", user_id: user.id, ip: req.ip, email: user.email, device_id });

// Kaidn returns the verdict; issuing the step-up is yours
if (r.reasons.includes("impossible_travel") || r.verdict === "block") return stepUp(user, r.event_id);
if (r.verdict === "review") return requireEmailConfirm(user, r.event_id);

Something to check this week#

Query your last ninety days for accounts whose recovery email changed within an hour of a login from a device never previously seen on that account. That single query, which costs nothing, finds takeovers that are already complete and tells you whether this is a problem you have or a problem you are worrying about.

Frequently asked questions

What is account takeover?

Account takeover is a stranger operating an account that is entirely real and belongs to somebody who trusted you. It is the inverse of every other pattern on this site, where the account was never real to begin with, and that inversion changes the detection problem completely: here you have months of genuine behaviour to compare against, and the question is not whether this looks risky but whether it looks like them.

Is a compromised account evidence that my security failed?

Usually not. Credential stuffing works on password reuse, and the passwords come from somebody else's breach. What you can do is detect the list being worked, which is visible across accounts even when each individual account sees only two or three failed attempts.

Will stronger password rules help?

Very little. Your users' passwords are already strong or weak elsewhere, and a complexity rule on your form does not change what they reused on a site that was breached two years ago. The lever that works is authentication a stolen password cannot satisfy.

What is the single most effective control?

Passkeys, and it is not close. They end credential stuffing for the accounts that adopt them in a way no detection layer can match. Detection is coverage for the accounts that have not migrated, for the recovery flows, and for phishing cases where the user handed over a live code. Anybody selling detection as a replacement for that is selling the second best option as the first.

Which flow should I harden if I can only harden one?

The recovery path, not the login. Login is where attention goes and where the attacker expects to be watched. The change of email address, phone number, passkey or payout destination is where a session turns into permanent ownership, and it is very often scored less carefully or not at all. Score those as their own events with the login that preceded them attached.

Score your own traffic for this

10,000 events a month free, no card. Every verdict comes back with the checks that fired and their weights, so you can see which signal caught it rather than trusting a number.

Other fraud types