V minulém článku jsme si popisovali, jaké techniky jsme v Shopsys využili pro řešení N+1 problému ve frontendovém GraphQL API. Dnes se podíváme na další aplikační postupy, kterými zajišťujeme bezpečí našeho API před nezvanými nájezdníky, kteří by rádi páchali škody.
Technologie GraphQL v základu uživateli nevymezuje žádné hranice pro skládání dotazů. To je jedna z jeho hlavních výsad, ale zároveň to může být i velký problém. Stačí jen trochu popustit uzdu fantazii a jedním dotazem si na API vyžádám kompletní katalog produktů spolu se všemi kategoriemi, k tomu všechny nejprodávanější produkty v kategoriích, k nim jejich značky, k těmto značkám opět všechny produkty se všemi prolinkovanými články, atd., atd. I v případě, že má API do posledního puntíku vyřešen N+1 problém, může takový dotaz způsobit problémy už jen tím, kolik dat musí aplikace v odpovědi přenést.
Jak můžu své API ochránit před přetížením?
Způsobů ochrany GraphQL API existuje několik. V Shopsysu kombinujeme všechny tři níže popsané metody, protože se navzájem pěkně doplňují.
1. Nastavení maximální hloubky dotazu
Na API lze určit, do jaké hloubky je povoleno pokládat dotazy. Stačí prozkoumat dotazy, které v aplikaci máme, najít ten s největším zanořením a toto nastavit jako maximální povolené zanoření. V reálné aplikaci taková hodnota pravděpodobně nebude příliš vysoká. Tímto nastavením lze jednoduše předejít nesmyslně hluboko zanořeným dotazům a odfiltrovat tak primitivní pokusy o přetížení API serveru.
2. Nastavení maximální velikosti stránky
Dalším užitečným a jednoduchým způsobem, jak ztížit útočníkům práci, je nastavení maximální velikosti stránky. Některá data (např. produkty) vrací API stránkovaně a uživatel si může sám říct, kolik záznamů na jednu stránku požaduje. Pokud jako limit použije nějaké hausnumero, může získat jedním dotazem kompletní katalog produktů. Protože ale v naší aplikaci takový scénář nepotřebujeme, můžeme velikost stránky produktů omezit třeba na 12 záznamů a dotazy na vyšší počet budou zamítnuty. Určitě stojí za zmínku, že toto omezení lze nastavit pro každou entitu zvlášť, takže např. pro výpis článků blogu si můžeme tento limit uzpůsobit opět dle skutečných potřeb naší aplikace.
3. Nastavení maximální povolené komplexity dotazu
Předchozí popsané techniky mohou pomoci se zabezpečením API, ale samy o sobě mohou být stále nedostačující. Útočník může sestavit dotaz s jednoduchou strukturou (splní limit pro maximální hloubku dotazu a zároveň nevyžaduje velké množství položek v jedné stránce), ale tento dotaz může být pro aplikaci z nejrůznějších důvodů stále velmi náročný. Je to tím, že pro aplikaci je získávání různých dat různě náročné. GraphQL počítá tzv. komplexitu dotazu sčítáním komplexity jednotlivých polí. Ve výchozím nastavení má každé pole hodnotu komplexity rovnu jedné, ale my víme, že např. dotaz na cenu produktu je náročnější než dotaz na jeho název. Naším úkolem je pak vytipovat všechna tato náročná místa a nastavit jim vyšší hodnotu komplexity. Po ohodnocení všech rizikových polí zkontrolujeme dotazy, které naše aplikace využívá, zjistíme výslednou komplexitu nejnáročnějšího aplikačního dotazu. Na API pak nastavíme, aby dotazy s vyšší komplexitou odmítlo.
Dynamický výpočet komplexity
Jak ve výpočtu komplexity zohlednit počet záznamů? Náročnost dotazu na produkty v kategorii by se měla logicky zvyšovat a snižovat dle toho, kolik produktů API reálně vrací. Toto je možné díky dynamickému výpočtu komplexity, do kterého může vstupovat i nastavení parametrů dotazu, např. tedy i celkový počet prvků, který si uživatel vyžádá. Výpočet pak funguje následovně. U dotazů vracejících kolekce entit si určíme komplexitu jedné položky, a výsledná komplexita je pak vypočítána jako násobek jednotkové komplexity a celkového počtu položek. Například pokud si nastavíme komplexitu pro pole products na hodnotu 5, dotaz products(first:10) bude mít výslednou komplexitu 5*10 = 50, zatímco komplexita dotazu products(first:1) se vyhodnotí jako 5*1 = 5. Pokud počet položek v dotazu nespecifikujeme, násobí se jednotková komplexita výchozím počtem položek, které API vrátí.
Jelikož na našich projektech máme pro API jediného klienta – samotný storefront e-shopu, mohli jsme popisované parametry nastavit dle konkrétních dotazů, které storefront využívá. Tento fakt nám zabezpečení API svým způsobem usnadnil, ale i kdyby API mělo být k dispozici dalším klientům, postup jeho zabezpečení by se v podstatě nezměnil. V takovém případě bychom museli hraniční hodnoty pro maximální hloubku, komplexitu a počet položek na jednu stránku nastavovat dle očekávaných způsobů užití API a případně nastavení upravovat dle relevantní zpětné vazby skutečných uživatelů API.