Skip to content

RateLimit – Ochrana proti floodu

Princip: Omezit počet requestů ze stejné IP adresy v daném časovém okně.

Používá se k ochraně login endpointů, registrace, API antes brute-force útoků. RateLimiter si pamatuje timestampy posledních requestů z každé IP adresy v JSON souborech a při překročení limitu request zamítne.

Proč se používá

  • Brute-force ochrana – Útočník nemůže zkoušet 1000 hesel za sekundu
  • DDoS mitigation – Sníží dopadu nevyškoleného DDoS scripperu
  • API rate limiting – Zamezí zneužití API endpointů
  • Login flood – Ochrana přihlašovacích formulářů

Jak to funguje (vnitřně)

Sliding Window Approach

Každá IP adresa má svůj JSON soubor v var/rate-limits/:

var/rate-limits/
├── a3f9c2d1e8b47f02.json  ← IP 192.168.1.100 (SHA-256 hash)
├── f7d2b1c9e3a4f6d8.json  ← IP 10.0.0.50
└── ...

Obsah souboru:

{
  "requests": [1742468100, 1742468105, 1742468110, 1742468115]
}

Pole requests obsahuje timestampy poslední aktivit (Unix timestamps). Přesné pořadí se neuchovává – jen hodnoty.

Jak se filtrují staré requesty

Při každém check():

  1. Načte pole timestampů
  2. Vyfiltruje ty, které jsou mimo časové okno (sliding window)
  3. Pokud zbývá málo requestů → přidá aktuální čas
  4. Pokud je víc → zamítne (vrátí false)

Příklad s check($ip, 5, 60) (5 requestů / 60 sekund):

čas 100: [100] → accept
čas 105: [100, 105] → accept
čas 110: [100, 105, 110] → accept
čas 115: [100, 105, 110, 115] → accept
čas 120: [100, 105, 110, 115, 120] → accept
čas 125: [100, 105, 110, 115, 120] → REJECT (6 requestů)
čas 162: [105, 110, 115, 120] → accept (100 expiroval, < 60s od teď)

Race Conditions a Locking

RateLimiter používá file locking (flock) na úrovni OS:

$fp = fopen($file, 'c+');
flock($fp, LOCK_EX);  // Exclusive lock – ostatní čekají
// ... read/write/modify ...
flock($fp, LOCK_UN);  // Release
fclose($fp);

To zabraňuje race conditions v high-concurrency prostředí (když více requestů z různých IP procesů souběžně zásahuje do var/rate-limits/).

Fail-open behavior: Pokud se nepodaří otevřít soubor nebo získat zámek, request je povolen (raději flood než false-positive block).

Výkon a Dopad na Přihlášení

Správný Výkon (Optimální Nastavení)

Jeden request: ~1-2ms

Pokud voláš RateLimiter jen na login endpointu, je to zanedbatelný overhead.

Zpomalení Přihlášení?

Ano, ale jen pokud voláš check() globálně na každý request (včetně statických souborů, AJAX, API):

// ❌ ŠPATNě – voláš na všechno
if (!RateLimiter::check($ip)) {
    http_response_code(429);
    exit;
}

Problém:

  • Login formulář = 10-15 requestů (HTML + CSS + JS + AJAX)
  • Všechny čekají na zámek stejného souboru
  • Fronta na zámku = viditelné zpomalení

Řešení

Volej RateLimiter jen tam, kde to má smysl:

// ✅ DOBŘE – jen na login
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $path === '/login') {
    if (!RateLimiter::check($ip, 10, 300)) {
        http_response_code(429);
        exit;
    }
}

Nebo filtruj statické soubory:

// ✅ DOBŘE – vynech statické assety
if (!in_array(pathinfo($path, PATHINFO_EXTENSION), ['js', 'css', 'png', 'jpg'])) {
    RateLimiter::check($ip);
}

Případ Použití

Scénář Limit Okno
Login form 10 pokusů 5 minut
API endpoint 100 requestů 1 minuta
Public form (kontakt) 5 1 hodina
Password reset 3 1 hodina

Ochrana Soukromí

  • IP adresy jsou hashované (SHA-256, 16 znaků)
  • Žádné plaintext IP se neukládá
  • Soubory mají TTL 1 hodinu (automaticky se mažou staré)
  • GDPR compliant

Přenositelnost & Nezávislost

RateLimiter je zcela přenositelný – žádné závislosti:

Žádné externí balíčky (Composer)

Žádné systémové příkazy (shell)

Žádné databáze (MySQL, PostgreSQL)

Žádné cache servery (Redis, Memcached)

Jen PHP built-in funkce:

  • fopen(), flock(), fwrite() – File I/O
  • json_encode() – JSON (PHP 5.2+)
  • hash('sha256') – SHA-256 hashing
  • mkdir() – Directory creation
  • time() – Unix timestamps

Filesystem: Jedinou závislostí je filesystem. Každý server/hosting má filesystém.

PHP verze: 7.1+ (standardní)

Kopírování na Jiný Projekt

# Zkopíruj jen core/RateLimit/:
cp -r core/RateLimit/ /your-project/

# To je vše!

Pak jen zavolej v boostrappingu:

RateLimiter::setPath('/path/to/rate-limits');

Nastavení Storage Path

NUTNÉ: Musíš zavolat setPath() během bootstrap aplikace!

// app/bootstrap.php
RateLimiter::setPath(DIR . '/var/rate-limits');
// Nebo vlastní cesta:
RateLimiter::setPath('/tmp/rate-limits');

Bez setPath() vyhodí RuntimeException (fail-fast design).

Výchozí Cesta

Neexistuje – musíš ji nastavit sám. Tím se zajistí explicitní inicializace a žádné "tiché selhání".

Lazy Cleanup

RateLimiter automaticky maže starší soubory (~1% šance na každý allowed request):

if ($allowed && random_int(1, 100) === 1) {
    self::cleanupOldFiles();  // Smaž soubory starší 1h
}

Zabraňuje neomezené akumulaci souborů na disku.

Další Čtení