Rate-limit / pokus-counter jako mrtvý kód (dotaz filtruje podle hádané hodnoty)
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”OTP/SMS verifikační endpoint má vizuálně kompletní brute-force ochranu — limit attempts >= 3 i strop „X kódů za hodinu”. Přesto je kód proti hádání 6místného kódu prakticky neomezený: každá špatná hádka vrátí „Invalid code” bez jakéhokoli započítaného pokusu.
Root cause
Sekce “Root cause”Verifikační dotaz filtroval řádek rovnou podle zadaného kódu:
const { data: verification } = await db .from('phone_verifications') .select('*') .eq('phone', phone) .eq('code', code) // ← filtr podle HÁDANÉ hodnoty .is('verified_at', null) .order('created_at', { ascending: false }) .limit(1).single();
if (!verification) return { error: 'Invalid code.' }; // špatná hádka končí TADY// ...if (verification.attempts >= 3) return { error: 'Too many attempts.' }; // nedosažitelnéawait db.from('phone_verifications') .update({ attempts: verification.attempts + 1 }).eq('id', verification.id);Když je hádka špatná, .eq('code', code) nenajde žádný řádek → funkce se vrátí dřív, než se k inkrementu attempts vůbec dostane. Counter roste jen u SPRÁVNÉ hádky, kde už na ničem nezáleží. Limit je mrtvý kód a útočník může zkoušet kódy bez omezení (do vypršení 10min platnosti).
Fix
Sekce “Fix”Vyber aktivní záznam podle identity (telefon), inkrementuj pokus VŽDY, a kód porovnej až v aplikační logice:
const { data: verification } = await db .from('phone_verifications') .select('*') .eq('phone', phone) // ← podle identity, NE podle kódu .is('verified_at', null) .order('created_at', { ascending: false }) .limit(1).single();
if (!verification) return { error: 'Invalid code.' };if (new Date(verification.expires_at) < new Date()) return { error: 'Expired.' };if (verification.attempts >= MAX_ATTEMPTS) return { error: 'Too many attempts.' };
// každá hádka (správná i špatná) se počítáawait db.from('phone_verifications') .update({ attempts: verification.attempts + 1 }).eq('id', verification.id);
if (String(verification.code) !== String(code)) return { error: 'Invalid code.' };// ... mark verifiedV kombinaci se „send” limitem (např. 5 kódů/hod) je celkový počet hádek omezen na ~25/hod proti prostoru 1 000 000 kódů — brute force neproveditelný.
Jak se tomu vyvarovat v jiných systémech
Sekce “Jak se tomu vyvarovat v jiných systémech”- Detection: najdi verify/OTP/token dotazy, kde se ve
WHERE/.eq()objevuje právě ta hodnota, kterou útočník hádá (code,token,otp,pin). Pak zkontroluj, zda inkrement counteru leží ZA early-returnem při „nenalezeno”. - Anti-pattern: „najdi řádek podle (identity + tajemství) → když nic, vrať chybu → jinak započítej pokus”. Pokus se započítá jen při úspěchu.
- Lepší přístup: oddělit lookup podle identity od porovnání tajemství. Counter/limit patří k identitě nebo k aktivnímu challenge řádku, ne k řádku vybranému tajemstvím. Porovnání tajemství dělej konstantně-časově v aplikaci.
Sister bugs / související
Sekce “Sister bugs / související”Stejný vzor hrozí u: reset-password tokenů (.eq('token', t)), e-mail verifikace, 2FA TOTP záloh, „magic link” kódů. Vždy, kdy je counter na stejném řádku, který se hledá tajemstvím.