← CodeClarityLab Home
Browse by Category
+ added · updated 7d
← Back to glossary

Account Takeover (ATO)

security CWE-287 OWASP A7:2021 CVSS 9.8 PHP 5.0+ Intermediate

Also Known As

ATO account compromise

TL;DR

An attacker gains full control of a user account through credential stuffing, phishing, session hijacking, or abusing password-reset flows.

Explanation

Account takeover aggregates multiple attack vectors: credential stuffing (testing breached username/password pairs at scale), password spraying (common passwords against many accounts), phishing (fake login pages), session hijacking (stolen cookies), and broken password-reset flows (predictable tokens, user-enumeration, host-header injection in reset emails). Defences form layers: bcrypt/argon2 password hashing, rate limiting login and reset endpoints, multi-factor authentication, breach-password detection (HaveIBeenPwned API), login anomaly detection (new device/IP), secure HttpOnly+SameSite session cookies, and short-lived password-reset tokens bound to the requesting IP stored as a hash.

Diagram

flowchart TD
    subgraph Attack Vectors
        CRED[Credential Stuffing<br/>leaked passwords]
        PHISH[Phishing<br/>fake login page]
        SIM[SIM Swap<br/>bypass SMS 2FA]
        SESS[Session Hijack<br/>steal cookie]
        RESET[Password Reset<br/>weak flow]
    end
    subgraph Defences
        MFA[Strong 2FA<br/>TOTP not SMS]
        RATE[Velocity checks<br/>per account]
        NOTIF[Login notification<br/>email on new device]
        CSESS[Concurrent session<br/>detection]
        RECOV[Secure recovery flow<br/>identity verification]
    end
    CRED --> MFA
    SIM --> MFA
    PHISH --> NOTIF
    SESS --> CSESS
    RESET --> RECOV
style MFA fill:#238636,color:#fff
style RATE fill:#238636,color:#fff
style NOTIF fill:#238636,color:#fff

Watch Out

Password-reset flows are the most common ATO entry point — sending a reset link to an attacker-controlled email after an unverified email-change is a critical flaw seen in many production systems.

Common Misconception

Account takeover always requires stealing a password. Attackers frequently take over accounts via password reset flaws, session fixation, or OAuth misconfigurations — no password needed.

Why It Matters

A compromised account gives an attacker full control of a user's data and identity; if that account has elevated privileges, the blast radius extends to all users.

Common Mistakes

  • Not invalidating all existing sessions after a password change.
  • Weak or predictable password-reset tokens that expire too slowly.
  • No rate-limiting on login, reset, or OTP endpoints.
  • Trusting client-supplied user IDs in session data rather than reading from the server-side session.

Avoid When

  • Do not implement security theatre (e.g. re-CAPTCHA only) without layered controls — bots solve CAPTCHAs routinely.
  • Avoid storing session tokens in localStorage — XSS can exfiltrate them; use HttpOnly cookies instead.

When To Use

  • Apply ATO defences on any account that controls money, data, or downstream user trust (admin accounts, payment accounts, OAuth providers).
  • Use this threat model when reviewing login, password-reset, session management, and email-change flows.

Code Examples

💡 Note
The bad example allows an email change with only a valid session — no re-authentication — so a stolen session cookie grants permanent account control.
✗ Vulnerable
// No re-auth required — attacker with stolen session can change email
public function updateEmail(Request \$req): Response {
    auth()->user()->update(['email' => \$req->email]);
    return response()->json(['ok' => true]);
}
✓ Fixed
public function updateEmail(Request \$req): Response {
    // Require current password for sensitive account changes
    if (!password_verify(\$req->current_password, auth()->user()->password)) {
        return response()->json(['error' => 'Password incorrect'], 403);
    }

    \$newEmail = \$req->validate(['email' => 'required|email|unique:users'])['email'];

    // Send verification to NEW address before applying
    \$token = bin2hex(random_bytes(32));
    Cache::put('email_change:' . \$token, ['user_id' => auth()->id(), 'new_email' => \$newEmail], now()->addHour());
    Mail::to(\$newEmail)->send(new VerifyEmailChange(\$token));

    return response()->json(['message' => 'Check your new email to confirm the change']);
}

Added 15 Mar 2026
Edited 31 Mar 2026
Views 27
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 2 pings F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W 1 ping T 1 ping F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W 1 ping T 2 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T
No pings yet today
No pings yesterday
Perplexity 8 Amazonbot 8 Google 2 Unknown AI 1 SEMrush 1 ChatGPT 1 Ahrefs 1
crawler 21 crawler_json 1
DEV INTEL Tools & Severity
🔴 Critical ⚙ Fix effort: High
⚡ Quick Fix
Audit the full authentication flow: password reset tokens (random_bytes), session regeneration on login, rate limiting on all auth endpoints
📦 Applies To
PHP 5.0+ web
🔗 Prerequisites
🔍 Detection Hints
Password reset using md5/time/mt_rand token or missing session_regenerate_id after login
Auto-detectable: ✗ No semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: High Context: File Tests: Update
CWE-287 CWE-620

✓ schema.org compliant