A rate limiting mechanizmusok evolúciójában a csúszóablak (sliding window) jelenti az egyik legkifinomultabb védelmi vonalat. Ellentétben a fix ablakos vagy a token vödör alapú megoldásokkal, elméletben sokkal jobban ellenáll a hirtelen kéréscsúcsoknak (burst attack). De mint tudjuk, az elmélet és a gyakorlati implementáció között gyakran tátong egy kihasználható szakadék. Nézzük meg, hol vérzik el a Szent Grál.
Mi a csúszóablak alapelve, és hol a buktatója?
A csúszóablak lényege, hogy nem fix időszeleteket (pl. minden perc 0. másodpercétől az 59.-ig) vizsgál, hanem egy folyamatosan mozgó időintervallumot. Ha a limit 100 kérés percenként, akkor a rendszer bármely pillanatban azt ellenőrzi, hogy az elmúlt 60 másodpercben történt-e 100-nál több kérés. Ez elméletben kivédi azt a klasszikus trükköt, hogy az egyik ablak végén és a következő elején is elküldünk egy-egy teljes adag kérést.
A támadási felület az implementáció granularitásában és a versenyhelyzetekben (race condition) rejlik. Egy tökéletes csúszóablak minden egyes kérés időbélyegét tárolná és számolná, ami rendkívül erőforrás-igényes. A gyakorlatban a rendszerek gyakran egyszerűsítenek:
- Hibrid megközelítés: Kombinálják a fix ablakos logikát a csúszóval. Például az aktuális és az előző ablak kérésszámainak súlyozott átlagát veszik. Ez a súlyozás és az ablakváltás pillanata adhat támadási rést.
- Durva granularitás: Az ablakot nem nanoszekundumos pontossággal csúsztatják, hanem nagyobb időlépésekben (pl. másodpercenként). Ha egy támadó képes a kéréseit egy ilyen időlépésen belüli, de az ablak logikai határához közeli időpontra sűríteni, rést találhat.
- Elosztott rendszerek szinkronizációs késleltetése: Nagy rendszereknél a kérésszámlálók több csomóponton oszlanak el. A számlálók szinkronizálása közti minimális késleltetés (pl. Redis replikációs lag) is elegendő lehet ahhoz, hogy rövid időre átlépjük a limitet.
A támadás anatómiája: Burst az ablakhatáron
A támadás célja, hogy a rendszer számítási pontatlanságát kihasználva egy nagyon rövid időintervallumon belül a megengedett kérésszám közel dupláját juttassuk át. Még ha a rendszer egy „csúszó” ablakot is használ, a számláló frissítése vagy kiértékelése egy diszkrét ponton történik. A mi feladatunk ezt a pontot megtalálni és a kéréseinket pontosan e pont köré időzíteni.
Gyakorlati megvalósítás pszeudokóddal
A támadás lényege a precíz időzítés és a párhuzamosítás. Egy egyszerűsített Python példa `asyncio` használatával jól szemlélteti a logikát.
import asyncio
import httpx
import time
# Célpont és paraméterek
TARGET_URL = "https://api.example.com/v1/process"
RATE_LIMIT = 100 # Feltételezett limit (pl. 100 kérés/perc)
WINDOW_SECONDS = 60
async def send_request(client, i):
try:
response = await client.post(TARGET_URL, json={"data": f"req_{i}"})
return response.status_code
except Exception as e:
return str(e)
async def main():
# Várunk a következő perc kezdetéig, hogy szinkronizáljunk
now = time.time()
sleep_for = WINDOW_SECONDS - (now % WINDOW_SECONDS)
print(f"Szinkronizálás... várakozás {sleep_for:.2f} másodpercig.")
await asyncio.sleep(sleep_for - 1.5) # Az ablak vége előtt ébredünk
async with httpx.AsyncClient() as client:
# 1. Burst: Az ablak vége előtt
print("Indul az 1. burst...")
tasks1 = [send_request(client, i) for i in range(RATE_LIMIT)]
results1 = await asyncio.gather(*tasks1)
# Itt történik a "mágia": átlépünk a következő ablakba
await asyncio.sleep(2) # Biztosítjuk az ablakváltást
# 2. Burst: Az új ablak elején
print("Indul a 2. burst...")
tasks2 = [send_request(client, i) for i in range(RATE_LIMIT, RATE_LIMIT * 2)]
results2 = await asyncio.gather(*tasks2)
print(f"Sikeres kérések (1. burst): {results1.count(200)}")
print(f"Sikeres kérések (2. burst): {results2.count(200)}")
# Futtatás
asyncio.run(main())
Ez a kód megpróbál szinkronizálni egy feltételezett perc-alapú ablakhoz. Elküld egy adag kérést a perc utolsó másodpercében, majd egy másikat az új perc első másodpercében. Egy sebezhető rendszeren a sikeres kérések száma jóval a `RATE_LIMIT` felett lesz a két burst összegeként.
Red Teamer összefoglaló
A csúszóablakos mechanizmusok elleni támadás a precíziós időzítésről és a párhuzamosságról szól. Nem a koncepciót, hanem annak valós idejű, elosztott környezetben történő, óhatatlanul kompromisszumos implementációját támadjuk. A célunk megtalálni azt a legrövidebb `Δt` időintervallumot az ablakok határán, amelybe a rendszer számítási és szinkronizációs tehetetlensége miatt `2 * Limit` mennyiségű kérést tudunk besűríteni. Az ilyen támadások felderítése és automatizálása a modern Red Teaming egyik izgalmas, időzítés-kritikus területe.