Safeguard
Application Security•

ASVS 5.0 V6.3.1: Enforce Credential Stuffing Controls

Implement the rate limits and anti automation your security documentation promises, and test that they hold. Level 1 guide with code and test steps.

Safeguard Security Team
4 min read

ASVS V6.3.1 requires that controls against credential stuffing and password brute force are actually implemented as your security documentation describes them. It is a Level 1 requirement and the working half of V6.1.1.

The documented design from V6.1.1 is only useful if it works at runtime. This requirement asks you to prove the controls exist, are switched on in production and survive real traffic patterns.

Why it matters

Stolen credential lists from other sites are tried against yours at scale. If login accepts unlimited attempts, an attacker only needs patience and a proxy pool.

Credential stuffing is quiet and distributed. A botnet sends one or two attempts per account from thousands of addresses, which slips under a simple per IP limit. Brute force on one admin account is louder, yet an account only lockout can be abused against you. Effective protection needs several signals together.

A useful mental model: the per account counter protects individual users, the per source counter protects the platform from noisy clients, and the global counter, which looks at the overall failure rate, tells you when an attack is under way even if each piece looks small.

How to implement it

Layer the controls. Throttle per account, per source, and per account and source pair. Increase delay instead of hard locking the victim.

import time
from redis import Redis
r = Redis()

def check_and_record(acct, ip, success):
    keys = [f"fail:acct:{acct}", f"fail:ip:{ip}"]
    limits = [5, 50]
    for k, lim in zip(keys, limits):
        n = int(r.get(k) or 0)
        if n >= lim:
            time.sleep(min(2 ** (n - lim), 30))   # progressive delay
            return "challenge"
    if not success:
        for k in keys:
            r.incr(k); r.expire(k, 900)
    else:
        r.delete(keys[0])
    return "ok"

Add bot detection or a challenge for abnormal volume, and return the same message whether the account exists or not.

Combine these layers.

  1. Per account progressive delay. Each failure adds to the wait for the next attempt on that account, from any source. The real user may be slowed, but not locked out.
  2. Per source throttle. Track failures per IP and per subnet, with a higher threshold for shared networks.
  3. Device binding. Issue a signed cookie to trusted devices so returning users bypass stricter limits.
  4. Challenge on suspicion. When the global failure rate spikes or a source looks automated, require a challenge or proof of work.
  5. Breached password rejection at sign in. If a correct password is known to be breached, require a reset after login.
  6. Uniform responses. Return the same message and similar timing for unknown users and wrong passwords.

Set the values in configuration and expose metrics: failed logins per minute, challenges issued, accounts under delay. Create an alert for sudden growth.

auth:
  login:
    per_account: { failures: 5, window: 15m, backoff: exponential, max_delay: 30s }
    per_ip:      { failures: 50, window: 10m, action: challenge }
    global:      { failure_rate_alert: 200/min }

How to test it

From a script, send 20 failed logins for one account and 100 from one IP across many accounts. Confirm the delays or challenges appear at the documented thresholds. Verify the real owner can still sign in from a different network.

Run an attack simulation in staging with a script. Send many wrong passwords for one account from one address and confirm the delay curve. Rotate through many addresses with one guess per account and confirm the global alert fires. Check that a legitimate sign in from a trusted device still works during the test.

Also check the less obvious entry points: the mobile API, SSO callback, password reset and the token refresh endpoint. Compare each result to the document, and file a bug where behavior differs.

Common mistakes

  • Limiting only by IP, which fails against distributed attacks.

  • Hard locking accounts, which hands attackers a denial of service.

  • Forgetting the API login and mobile endpoints.

  • Putting the rate limit in a front end load balancer that internal clients bypass.

  • Trusting a forwarded header for the client address without validating the proxy chain, which lets attackers spoof their source and avoid limits.

  • Resetting counters on any success, including the attacker's own valid account.

Try it yourself

Click through it at your own pace, or open it full screen.

Quick check

Why use per account and per IP limits together?

Per account stops focused attacks on one user, per IP slows spraying across many users.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.