Safeguard
Application Security•

ASVS 5.0 V2.4.1: Add Anti-Automation Controls to Costly Functions

ASVS 5.0 V2.4.1 (Level 2) requires controls against excessive calls to app functions. Rate limit patterns, quotas, and load tests to prove they work.

Safeguard Security Team
5 min read

ASVS 5.0 V2.4.1 requires anti-automation controls that protect against excessive calls to application functions, which could cause data exfiltration, garbage data creation, quota exhaustion, rate limit breaches, denial of service or overuse of costly resources. It is a Level 2 requirement.

Why it matters

Scripts do what humans cannot: ten thousand sign ups a minute, a scrape of every record through an ID counter, a flood of expensive search or export requests, or a loop that burns your paid email, SMS or AI budget. Without limits, one client can take down or bankrupt a feature for everyone.

How to implement it

Pick the functions that are costly, sensitive or enumerable: login, sign up, password reset, search, export, file upload, messaging, and anything that calls a paid service. Then limit each by identity as well as address, since addresses are easy to rotate.

A token bucket per user, enforced in shared storage, works across many instances:

import time

# Atomic token bucket: the read and the update happen in one Redis call
LUA = """
local t = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local rate, burst, now = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
local tokens = tonumber(t[1]) or burst
local ts = tonumber(t[2]) or now
tokens = math.min(burst, tokens + (now - ts) * rate / 60)
local ok = 0
if tokens >= 1 then tokens = tokens - 1; ok = 1 end
redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', KEYS[1], 3600)
return ok
"""

def allow(redis, key, rate_per_min, burst):
    return redis.eval(LUA, 1, key, rate_per_min, burst, time.time()) == 1

The script makes the read and update atomic, so parallel requests cannot race. Your gateway's built in limiter does the same job if you have one. Return HTTP 429 with a Retry-After header. Add layers: per account, per address, and global ceilings. Use pagination caps and maximum page sizes, and cost based limits for expensive queries. For sign up and reset flows, add verification or a challenge when behaviour looks automated. Alert when limits trigger repeatedly.

How to test it

Script a burst against each protected endpoint with a load tool and confirm that requests above the limit receive 429 and the service stays healthy. Test across multiple instances, since per instance counters multiply the real limit. Try rotating addresses with the same account, and a fresh account from the same address. Check that an expensive endpoint, such as export, has a lower limit than a cheap one.

Choosing limits that hold up

Start from cost. An endpoint that triggers an email, an SMS, a file conversion or a call to an external API should have a much lower limit than a cheap read. Write the limit next to the cost in your documentation, tying this back to V2.1.3.

Layer the limits

Use several keys at once: per account, per source address, per target (for example per recipient email address), and a global ceiling per function. An attacker who beats one layer still hits another. Per target limits stop a script from using your reset endpoint to harass one victim.

Fail safely and say so

When the limiter store is down, decide in advance whether to allow or deny, per endpoint. Login and payment endpoints usually fail closed. Return a clear 429 with Retry-After, so well behaved clients back off, and avoid error messages that reveal how the limit is computed.

Bulk and enumeration

Cap page sizes, batch sizes and export row counts. Use unguessable identifiers where enumeration would leak data, and slow or challenge sequential access patterns that walk through records in order.

Prove it with a load test

A short script is enough to prove a limit works:

for i in $(seq 1 50); do
  curl -s -o /dev/null -w "%{http_code}\n" https://staging.example.com/api/reset -d 'email=a@example.com'
done | sort | uniq -c

You should see a mix of 200 and 429 results, not fifty 200 lines. Run it against every instance, or through the load balancer, to confirm the counters are shared.

Common mistakes

  • Limiting only login and nothing else.
  • Counters kept in one server's memory.
  • Limits keyed only on IP address.
  • No cap on page size or on batch request length.

Try it yourself

See how an API scan exercises endpoints and spots missing limits.

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

Quick check

Why not key limits only on IP address?

Attackers rotate addresses cheaply, and many real users share one. Combine address and account based limits.

What status code should a limited request get?

429 Too Many Requests, ideally with a Retry-After header.

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.