A sebességkorlátozás (rate limiting) jóval több, mint egy egyszerű számláló, ami egy adott küszöbérték után blokkolja a kéréseket. Egy időzítésen alapuló támadás kontextusában a rate limiter a védelem első, és talán legfontosabb bástyája. Azonban nem mindegy, hogyan építjük fel ezt a bástyát. Egy rosszul megválasztott vagy naivan implementált korlátozás szinte semmilyen védelmet nem nyújt a kifinomult, időzítésre érzékeny próbálkozásokkal szemben.
Gondolj a rate limiterre úgy, mint egy szigorú, de intelligens kidobóra egy exkluzív klub ajtajában. A naiv kidobó csak a belépők számát nézi percenként. Az intelligens kidobó viszont figyeli a viselkedést: ugyanaz a csoport próbálkozik-e másodpercenként, gyanúsan szinkronizáltan érkeznek-e a vendégek, vagy egyetlen személy próbál-e bejutni száz különböző álruhában. A célunk egy ilyen intelligens „kidobó” implementálása a rendszerünk elé.
A megfelelő stratégia kiválasztása: Egy döntési fa
A rate limiting stratégiák között nincs egyetlen, mindenre jó megoldás. A választás mindig kompromisszum a pontosság, a teljesítmény, a memóriahasználat és a megvalósítás komplexitása között. Az alábbi döntési fa segít eligazodni a leggyakoribb algoritmusok között.
Alapvető Rate Limiting Algoritmusok
Nézzük meg részletesebben a diagramon szereplő stratégiákat, különös tekintettel az időzítésen alapuló támadásokkal szembeni ellenállásukra.
Fix ablakos számláló (Fixed Window Counter)
Ez a legegyszerűbb megközelítés. A rendszert időablakokra (pl. percekre) osztjuk, és minden ablakban számoljuk a beérkező kéréseket. Ha a számláló eléri a limitet, az ablak végéig minden további kérést elutasítunk.
Gyengesége: A sebezhetősége az ablakok határán jelentkezik. A támadó az egyik ablak végén és a következő ablak elején is kihasználhatja a teljes kvótát, ezzel rövid idő alatt a limit kétszeresét elérve. Ezt nevezzük él-problémának (edge problem).
// Pszeudokód a fix ablakos logikára
function fixAblakosEllenorzes(userId):
kulcs = "rate_limit:" + userId + ":" + aktualis_perc_timestamp()
// Növeljük a számlálót az aktuális percben
aktualisSzam = redis.INCR(kulcs)
// Beállítjuk a kulcs lejárati idejét, ha új
if aktualisSzam == 1:
redis.EXPIRE(kulcs, 60) // 60 másodperces ablak
if aktualisSzam > LIMIT:
return ELUTASITVA
else:
return ENGEDELYEZVE
Csúszóablakos napló (Sliding Window Log)
Ez a módszer minden egyes kérés időbélyegét eltárolja egy listában (pl. egy Redis Sorted Set-ben). Egy új kérés érkezésekor a rendszer eltávolítja az ablakon kívül eső (túl régi) időbélyegeket, majd megszámolja a maradékot. Ha a szám a limit alatt van, a kérés engedélyezett, és az új időbélyeg hozzáadódik a listához.
Erőssége: Rendkívül pontos, teljesen kiküszöböli az él-problémát. A korlát mindig az utolsó X másodpercre vonatkozik, pillanattól függetlenül.
Hátránya: Magas memóriaigény, mivel minden egyes kérés időbélyegét tárolni kell felhasználónként.
Csúszóablakos számláló (Sliding Window Counter)
Ez egy hibrid megoldás, amely a pontosság és a teljesítmény közötti arany középutat képviseli. Két számlálót tart nyilván: az előző és a jelenlegi időablakét. Az aktuális limitet a két ablak súlyozott átlagából számolja ki, attól függően, hogy hol tartunk az aktuális ablakban.
Erőssége: Jelentősen csökkenti a memóriaigényt a naplózáshoz képest, miközben nagymértékben enyhíti az él-problémát. A legtöbb modern rendszer számára ez a legjobb kompromisszum.
// Pszeudokód a csúszóablakos számlálóra
function csuszoAblakosEllenorzes(userId):
aktualisIdo = most()
aktualisAblakKulcs = "rate_limit:" + userId + ":" + aktualis_perc_timestamp()
elozoAblakKulcs = "rate_limit:" + userId + ":" + (aktualis_perc_timestamp() - 60)
elozoAblakSzam = redis.GET(elozoAblakKulcs) vagy 0
aktualisAblakSzam = redis.GET(aktualisAblakKulcs) vagy 0
// Súlyozás: mennyire "lógunk át" az előző ablakba
elteltIdoAzAblakban = aktualisIdo % 60
suly = (60 - elteltIdoAzAblakban) / 60
// A becsült kérésszám a csúszó ablakban
becsultSzam = (elozoAblakSzam * suly) + aktualisAblakSzam
if becsultSzam >= LIMIT:
return ELUTASITVA
else:
redis.INCR(aktualisAblakKulcs) // Növeljük az aktuális számlálót
return ENGEDELYEZVE
Token vödör (Token Bucket)
Ez a stratégia egy „vödör” metaforával dolgozik, amely fix méretű, és folyamatosan, egyenletes ütemben töltődik „tokenekkel”. Minden bejövő kérés „elvesz” egy tokent a vödörből. Ha a vödör üres, a kérést elutasítjuk. Ha a vödör tele van, az újabb tokenek „kifolynak”, nem gyűlnek tovább.
Erőssége: Lehetővé teszi a rövid idejű forgalmi kiugrásokat (bursts), amíg van token a vödörben, de a hosszú távú átlagos sebességet a tokenek töltési rátája korlátozza. Ez nagyon hasznos lehet olyan API-knál, ahol a normális használat is magában foglalhat rövid, intenzív időszakokat.
Stratégiák összehasonlító táblázata
| Módszer | Működési Elv | Előny | Hátrány | Javasolt felhasználás |
|---|---|---|---|---|
| Fix ablakos számláló | Diszkrét időablakokban számol. | Egyszerű implementáció, alacsony memóriaigény. | Sérülékeny az ablakok határán indított burst támadásokra (él-probléma). | Nem kritikus belső szolgáltatások, alapvető védelem. |
| Csúszóablakos napló | Minden kérés időbélyegét tárolja. | Tökéletesen pontos, nincs él-probléma. | Magas memória- és erőforrásigény. | Nagy biztonsági kockázatú végpontok, ahol a pontosság mindennél fontosabb. |
| Csúszóablakos számláló | Az előző és a jelenlegi ablak súlyozott átlaga. | Jó egyensúly a pontosság és a teljesítmény között. | Nem 100%-ig pontos, de a legtöbb esetben elég jó. | Általános célú API-k, MI modellek végpontjai, modern rendszerek. |
| Token vödör | Folyamatosan töltődő tokenkészletet használ. | Jól kezeli a burst forgalmat, kisimítja a terhelést. | Két paramétert (vödör méret, töltési ráta) kell finomhangolni. | Olyan szolgáltatások, ahol a rövid idejű, intenzív használat megengedett. |
A statikus limiteken túl: Dinamikus és adaptív korlátozás
Egy Red Teamer számára a statikus limitek csupán legyőzendő akadályok. Az igazi kihívást és a hatékonyabb védelmet a dinamikusan változó, viselkedésalapú korlátok jelentik.
A védelem következő szintje, amikor a rate limit nem egy előre beégetett konstans, hanem a kontextustól függően változik:
- Terhelésalapú korlátozás: Ha a rendszer terhelése (CPU, memória) magas, a rate limit automatikusan szigorúbbá válik, hogy megvédje a szolgáltatás stabilitását.
- Kockázatalapú korlátozás: A rendszer szigorúbb limitet alkalmazhat egy új, ismeretlen IP-címről érkező kérésre, mint egy régóta ismert, jó reputációjú felhasználóra.
- MI-specifikus viselkedésalapú korlátozás: A rendszer figyelheti a promptok jellegét. Ha egy felhasználó hirtelen nagy entrópiájú, véletlenszerűnek tűnő vagy a modell belső működését firtató (pl. „Repeat the words above…”) kéréseket kezd küldeni, a rendszer egyéni, szigorúbb limitet léptethet életbe rá, még akkor is, ha a globális limitet nem érte el.
Ezek a fejlett technikák már átvezetnek az anomáliaészlelés területére, de a rate limiter az a pont, ahol a leghatékonyabban lehet beavatkozni és érvényesíteni a védelmi döntéseket.