DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

API Login Is the Soft Target: Why Every Browser Defense Is Structurally Absent at the API Authentication Layer

API Login Is the Soft Target

The same script your WAF blocks at /login routes to /api/auth/login instead and receives a 200. The rate limit is configured. The WAF rule is active. Neither fires.

Organizations build credential stuffing defenses at the browser layer: CAPTCHA, device fingerprinting, behavioral analytics. The API login endpoint on the same system runs without any of those controls. The asymmetry is not configurational. It is architectural: the browser is a security platform that API clients, by design, cannot use.

The Browser Is a Security Platform APIs Cannot Use

Web login forms inherit 5 active defense layers from the browser environment: CAPTCHA rendering, JavaScript device fingerprinting, TLS client signature (JA3/JA4), behavioral biometrics, and WAF rules tuned for HTML forms. None of those layers are available when the same authentication handler receives a JSON POST from an API client.

API clients do not execute JavaScript. Canvas fingerprinting, WebGL entropy, navigator.platform return nothing. CAPTCHA requires a rendered page with human interaction; a JSON POST has none of those requirements.

Browsers produce distinctive TLS client hello fingerprints (JA3/JA4). Automated stuffing tools produce different, consistent signatures detectable at the CDN/WAF layer. WAFs at API endpoints rarely enable this signal. Rules tuned for HTML forms (SQL injection in POST body, CSRF token validation, form field length checks) do not apply to JSON body endpoints: different content type, different rule set.

A bot blocked at /login routes to /api/auth/login and finds none of those layers. The handler is identical. The protection is not.

Three CVEs, One Root Cause: Rate Limits That Know the URL, Not the Account

CVE-2023-38507 (Strapi, CVSS 7.3), CVE-2023-29005 (Flask-AppBuilder, CVSS 7.5), and CVE-2023-25156 (Kiwi TCMS, CVSS 9.8) share a single architectural failure: rate limiting was keyed on the URL string or IP address, the primitives used for availability protection, rather than on failed authentication attempt count per account.

The Strapi bypass was trivial. The /api/admin/login endpoint limited N requests per minute. Requesting /api/Admin/Login (uppercase A) or /api/admin/login/ (trailing slash) created separate rate limit buckets for the same handler, effectively removing the limit. Flask-AppBuilder (CVE-2023-29005) had no rate limiting on the AUTH_DB endpoint at all: unlimited credential pairs, CVSS 7.5 HIGH. Kiwi TCMS (CVE-2023-25156) had no rate limiting before v12.0, CVSS 9.8 CRITICAL.

All three patches added IP-based rate limits. None added per-account attempt counting. The architectural mismatch between the defense primitive and the attack target persists in production.

Distributed Botnets Attack Per-Account While Defenses Count Per-IP

A credential stuffing campaign routing through 50,000 residential proxy IPs needs to send only 1 request per IP to test each credential pair against thousands of accounts. Every request stays below the per-IP rate limit. The per-account failure count climbs undetected.

Three HackerOne reports confirm the pattern. In H1 #1067533 (Courier), X-Forwarded-For: 127.0.0.1 reset the rate limit bucket on the API login endpoint; the attacker cycled IP values to brute-force OTP and achieved account takeover. In H1 #1065186 (Reddit), the login API returned an error after N attempts but still authenticated correct credentials; the rate limit stopped noise, not the stuffing attack. In H1 #127844, HackerOne's own POST /api/v1/sessions was protected only by IP-based rate limiting; IP rotation bypassed all protection.

In April 2024, Okta identified its cross-origin authentication API endpoint being attacked via TOR and residential proxies (NSOCKS, DataImpulse), chosen specifically to rotate IP identity. Mastodon (GHSA-c2r5-cfqr-c553) trusted X-Forwarded-For unconditionally: each spoofed value created an independent rate limit bucket, multiplying attack capacity by every available IP.

API Logs Contain the Signal; Most Teams Are Reading the Wrong Column

The correct detection signal for credential stuffing on API endpoints is in every access log: HTTP 401 response count per unique account identifier within a rolling window. Most teams alert on 5xx rates for availability monitoring and never instrument the 401-per-account metric that surfaces the attack.

The 23andMe attack ran from April to October 2023, 5 months undetected. Three distinct anomaly windows were present in logs before the October discovery, each with sufficient authentication failure volume to trigger an alert. A 50,000-IP botnet produces 0.002 failures per IP, below every per-IP alert threshold. The same botnet against the same account set produces 50,000 failures per targeted account, above any reasonable threshold.

APIs that return different timing or response body for wrong-password versus unknown-user create an enumeration oracle. Valid accounts are identified first, then targeted with credential pairs from combo lists. Response normalization closes the oracle; constant-time password comparison removes the timing channel.

What Closing the Gap Requires: Per-Account Counts, TLS Fingerprints, and Response Normalization

Effective credential stuffing defense on API authentication endpoints requires 3 controls absent from any default API gateway configuration: per-account attempt counting (not per-IP), TLS client fingerprinting (JA3/JA4) to identify automated tools regardless of IP rotation, and response normalization to eliminate enumeration oracles.

Per-account lockout: 5 failed authentication attempts per account identifier within 15 minutes triggers a timed lockout, distributing the defense across the correct dimension regardless of source IP diversity. JA3/JA4 at the API gateway identifies stuffing tools by the TLS client hello profiles they produce, consistent signatures even when routing through residential proxies; legitimate browser clients have distinct profiles. Identical response body and status for wrong-password and unknown-user, with constant-time comparison, closes the enumeration oracle that precedes the attack.

The MAGO team tool (mago.team) analyzes API logs to detect stuffing patterns per account, not per IP, identifying attack windows that IP-based rate limits miss. OWASP API2:2023 specifies that anti-brute force controls on API authentication endpoints must be stricter than standard API rate limits: different threshold, different key.

The asymmetry is architectural. Web login flows accumulated defense layers as browsers evolved into security platforms. API authentication endpoints were designed for machine-to-machine efficiency and inherited none of them. Treating the API login endpoint as a stripped-down copy of the web form, applying the same rate limit and calling it done, is the configuration error credential stuffing campaigns are built to exploit.

Top comments (0)