Cross Site Scripting pořádně

Školení, která pořádám

Včera vyšly o XSS dva články nevalné kvality. Horší je to tím, že vyšly na serverech, které jsou mnohými považované za autoritativní (mělo by se jim dát věřit). V čem jsou články špatné?

Kdybych já psal článek o XSS, tak v první řadě uvedu, že si musíme uvědomit, že v některých kontextech mají některé znaky speciální význam a musíme je tedy ošetřit. V HTML textu (tedy mezi značkami) to jsou znaky < a &, v hodnotě atributu uzavřené do uvozovek potom " a & (kvůli validitě XHTML ještě <). V jiných kontextech (název značky, název atributu, neuzavřená hodnota atributu, obsluha událostí, JavaScriptový kód, styly) je lepší uživatelský vstup vůbec nepoužívat. Pokud to je nutné, je vhodné použít whitelist. Pozor je potřeba si dát taky na vkládání externích URL (odkazy a obrázky).

Pokračoval bych tím, že všechny tyto znaky se v PHP dají ošetřit funkcí htmlspecialchars, která jako bonus ošetří navíc i znak > (neošetřený vypadá v HTML kódu jako nahý, v XHTML má speciální význam sekvence ]]>). Funkci lze navíc konstantou ENT_QUOTES předanou v druhém parametru přesvědčit, aby ošetřovala i apostrof, což využijeme u hodnot atributů uzavřených do apostrofů. Tuto funkci používáme kdykoliv do HTML dokumentu vypisujeme data, která se nemají interpretovat jako HTML.

Článek bych ukončil tím, že pokud chceme jakkoliv ošetřovat výstup, musíme si být jisti, v jakém je kódování. U HTML dokumentů je potřeba kódování uvést v hlavičce Content-Type a ve značce <meta>.

Pokud by mi ještě zbylo místo, doplnil bych efektivní obdobu funkce htmlspecialchars pro JavaScript, použitelnou třeba při nastavování vlastnosti innerHTML:

/** Nahrazení znaků <>&" HTML entitami
* @param string řetězec se speciálními znaky
* @return string ošetřený řetězec
*/
function htmlspecialchars(text) {
	return text.replace(/[<>&"]/g, function (s) {
		switch (s) {
			case '<': return '&lt;';
			case '>': return '&gt;';
			case '&': return '&amp;';
			case '"': return '&quot;';
		}
	});
}

Přijďte si o tomto tématu popovídat na školení Bezpečnost PHP aplikací.

Jakub Vrána, Výuka, 6.3.2009, diskuse: 33 (nové: 0)

Diskuse

ikona Chamurappi:

Při dosazování do innerHTML stačí v HTML jednodušší replace:
něco.innerHTML = text.replace(/[<&]/g, "$&<!>");
— řídicí znaky ztrácí svoji kouzelnou moc, pokud jim na záda dýchá něco, s čím nemohou kouzlit (třeba komentář).

V XML jazycích je vhodné zapsat entitou i znak „>“, protože sekvence znaků „]]>“ neukončující CDATA sekci (a mimo hodnotu atributu) musí dle specifikace celý dokument pohřbít.

ikona Jakub Vrána OpenID:

Děkuji za doplnění. Trik s <!> by mě nenapadl a v HTML dokumentu i trochu vyděsil, ale formálně to máš jistě v pořádku. Díky i za vysvětlení speciálního významu >.

ikona david@grudl.com:

Amen.

Jozef Sevcik:

Uprimne sa Vam cudujem. Snazit sa vyvratit nazory samozvaneho bezp. experta, ktory tu na tomto blogu este pred nejakou dobou tvrdil ze hashe sa daju zpetne dopocitavat (http://php.vrana.cz/slozitost-hesel.php#d-6473) mi pride ako strata casu.
V Turkovom pripade plati 'Pokial sa budete tvarit ako expert, budu si o vas mysliet ze nim naozaj ste.'

ikona David Grudl:

A proč by pro krátké řetězce nešly? Dokonce za použití rainbow tables v poměrně krátkém čase (http://en.wikipedia.org/wiki/Rainbow_table)

ikona Jakub Vrána OpenID:

To samozřejmě ano, článek nad odkázaným diskusním příspěvkem o tom dokonce pojednává. Ale Rastislav Turek napsal "ak je heslo kratsie ako 32 znakov, je ho mozne spatne dopocitat". Uznej, že to by byl převrat v kryptologii.

ikona david@grudl.com:

Teď jsem si to četl celé a je to samozřejmě děsný blábol. Zklamání...

ikona Rastislav Turek:

Ach veru, zle zvolene slovo. Za dopocitat som myslel hrubou silou zistit. Slo mi len o vysvetlenie dovodu saltovania v md5, ktore som si zvolil za priklad. Prave napriklad vdaka rainbow sa dnes daju zohnat celkom rozsiahle tabulky http://www.freerainbowtables.com/en/tables/md5/ a samozrejme vygenerovat daleko obsiahlejsie. nechcel som tym povedat, ze by bolo md5 rozkodovatelne na zaklade algoritmu. ale ano, zle som to napisal, to je pravda.

ikona Jakub Vrána OpenID:

Tento příspěvek dokazuje tvé zmatení, pokusím se to tedy vysvětlit.

Zahešovaná jednoduchá hesla lze rozluštit vyzkoušením všech možností. Pokud se hešovalo samotné heslo, jde dohledat v Rainbow Tables. Pokud byl před hešováním k heslu přidán libovolně dlouhý salt a tento salt je útočníkovi znám, může útočník vyzkoušet všechny možnosti vlastním výpočtem.

Zbývá vyřešit otázku, co je to „jednoduché heslo“ – záleží na pestrosti použitých znaků a délce. Např. tebou odkázaná tabulka loweralpha-numeric-space#1-8 dovoluje na každém místě použít 37 znaků (26+10+1). Při celkové délce 8 znaků to je tedy 37^8 ~ 4e12 možností (k tomu nějaké drobné za kratší hesla). Tolik možností lze na současných počítačích skutečně vyzkoušet (nebo v podobě Rainbow Tables stáhnout).

Tvůj výrok, že hesla kratší než 32 znaků lze dopočítat (neboli hrubou silou zjistit), by znamenal, že lze vyzkoušet 37^32 ~ 2e50 možností (pro jednoduchost předpokládám stejnou sadu 37 znaků). To na současných počítačích opravdu nejde.

ikona Rastislav Turek:

Ziadne zmatenie tam nie je. Mne je samzorejme jasne, ze vytvorit 32 miestnu kombinacie vsetkych dostupnych znakov nie je dnes realizovatelne. ja som sa len snazil vysvetlit dovod pouzivania saltu na priklade md5. ak hash zastupuje realnu hodnotu, tak je vzdy problem, ked je ju mozne ziskat nejakym sposobom. to ze to dnes nejde neznamena ze to nepojde o 10 rokov. takto sa to stalo aj s jednoduchsimi heslami, kedy rainbow dnes umoznuje priam bezproblemovo ziskat hesla o celkom zaujiavych dlzkach. u kazdeho bezp. riesenia je potrebne sa pripravit co najlepsie na vsetky mozne scenare, aj keby rovno nenastali.

ale ano, zle som to vyjadril, mohol som to lepsie opisat.

ikona Jakub Vrána OpenID:

Ale ty ses nevyjadřoval takhle obecně, uvedl jsi zcela konkrétně „ak je heslo kratsie ako 32 znakov, je ho mozne spatne dopocitat“. To není nešťastné vyjadřování, to je prostě nesmysl. A i kdybych připustil výhled do budoucna, tak konkrétně 32 znaků bude možné při stejném tempu zrychlování počítačů (aproximované Mooreovým zákonem) vypočítat za 188 let. To je opravdu odvážný výhled.

Navíc jsi zcela pominul, že ani dlouhý salt neochrání jednoduché heslo, pokud útočník tento salt zná.

ikona Rastislav Turek:

Samozrejme, ze som sa nevyjadroval obecne. Vyjadroval som sa na urovni moznosti md5, kedy je md5 hash do 32 znakov obsahuje svoj zdroj, teda je spatne dopocitatelny (brute force). Kam pojde vyvoj pocitacov, procesor, ci sa neobjavi iny algoritmus si nedovolim odhadnut, pretoze nie som nostradamus. pri hladani bezpecnostnych rizik sa musis pozerat na moznosti danej ochrany. neviem, ci bude v najblizsich 10 rokoch mozne dekodovat akymkolvek sposobom hocijaky md5 hash, ale nespolieham sa na to, ze by to tak nebolo. preto som to napisal velmi konkretne. limit md5 pozname vsetci, teda vsetko po tento limit je momentalne teoreticky nebezpecne.

a co sa tyka saltu, je predsa jasne, ze ak by ho utocnik poznal, tak straca zmysel. to je predsa samotna podstata saltu http://en.wikipedia.org/wiki/Salt_(cryptography)

ikona Jakub Vrána OpenID:

Pleteš se. Dopočítat se dají všechny haše, záleží jen na výpočetní složitosti. Žádná hranice 32 znaků ve smyslu, že kratší hesla by byla nebezpečná a delší jsou bezpečná, neexistuje. „Limit MD5“ je to, co tě mate. Délka výstupu nemá nic společného s minimální délkou bezpečného vstupu.

Smysl saltu je i v tom, že znemožňuje dohledat haš v Rainbow Tables a že neprozradí uživatele se stejnými hesly (pokud je salt u každého uživatele jiný). To platí i v případě, kdy se útočník k saltu dostane.

ikona Rastislav Turek:

Ah, mas recht. Zle som si to priradil k dlzke hashu, neviem preco.

peter:

Md5 je vysledek presne zname funkce kontrolniho souctu. V nem lze stanovit, jakych hodnot za jakych vstupnich podminek budou nabyvat jednotlive casti souctu. Pak je mozne dopocitat pro zacatek stovky ruznych moznosti, ktere davaji stejny md5 kod. A kdyz se to spravne omezi, vidim jako realnou moznost dopocitat i skutecne heslo (1-20 rekneme) v relativne solidnim case do 2 dnu.
Nekde o tom byl clanek, jak pocitat prvni z nekonecna moznosti do 5s. Nez to vypise uzivatelovo heslo, tak to chvilku potrva.

JS replace s tim komentarem je zajimavy.

Roman:

ve funkci htmlspecialchars(text) chybí ) před poslední }

ikona Jakub Vrána OpenID:

Díky za upozornění, opravil jsem to.

ikona Rastislav Turek:

Bohuzial som bol mimo mesta, ale uz som ti snad dal odpoved na to, preco osetrovat aj podla teba neskodne znaky
http://blog.synopsi.com/2009-03-04/ako-na-…#comment-4069

zaujima ma ale miesanie vstupnej a vystupnej kontroly. uz som sa o tom rozpraval s davidom. ak by si prevadzal danu kontrolu na vstupe do aplikacie, urcite by si si ocistil databazu, ale co ak niekto infikuje databazu priamo? potom je vlastne opatrenie neucinne. rovnako je to aj vtedy, ak zle osetris tretiu cestu, ako napriklad api, alebo dokonca aplikacie, ktore budu s databazou priamo pracovat a ktore sa mozu stat zdrojom utoku.

ikona Jakub Vrána OpenID:

Reakce na obhajobu ošetřování neškodných znaků je u tebe. Zde jen stručně – převod uvedených znaků na entity nic nevyřeší, protože se v prohlížeči stejně interpretují jako ty znaky.

Co se míchání vstupní a výstupní kontroly týče – aplikace by měla vevnitř pracovat jen s daty, která jsou v pořádku (pokud je např. aplikace v UTF-8, tak by všechny řetězce měly být interpretovatelné v tomto kódování). Než aplikace tedy přijme např. data od uživatele a něco s nimi udělá (pošle do databáze, vypíše na výstup), měla by je ověřit. Pak už se celá aplikace může spolehnout na to, že jsou všechna data v pořádku. To platí o všech vstupech, např. i o datech získaných aplikací z databáze (např. MySQL po deklaraci používaného kódování vrací texty právě v tomto kódování a pokud se do textového řetězce nesnažím načíst např. obsah BLOBu, tak už nic ověřovat nemusím). Vevnitř aplikace jsou tedy data korektní a můžu s nimi dělat cokoliv, aniž bych je musel jakkoliv kontrolovat. Při výstupu pak stačí tato korektní data ošetřit pro výstupní vrstvu.

ikona Rastislav Turek:

Znaky preberame u mna, takze tam som ti na to aj odpovedal. Tu mas opatovne priklad, ktory to snad konecne povie za mna (teda ze sa s entitami mylis) http://synopsi.com/poc.html

Nemozem viac nez suhlasit, ze by aplikacia mala pracovat uz s cistymi datami, aby nebola databaza zapratavana sajrajtom. Nuz, na vacsich projektoch, kde pracuje za prve viac programatorov, alebo kde sa aspon vystriedaju dve tri spolocnosti to funguje malinko inak. Za prve, ak ma do DB pristup softver, ktory kodila ina spolocnost, ako sa mozes spolahnut, ze sa tade nic nedostane. Rovnako to moze prist z mnozstva inych ciest. Ak budes vsetko osetrovat na vstupe a niekto si vstup dovytvori, tym ze priamo pracuje s DB a obchadza tvoje procesy osetrovania, tvoja security je neucinna ale tvoj web bude infikovany, ak sa nieco stane, pretoze si na vystupe nic neurobil.
Za druhe, zdielane hostingy. Neraz sa stalo, ze boli objavene local vulns. Ak ti teda niekto lokalne infikuje databazu a prida tam jednoduche XSS do DB, ktore bude sirit malware, opat je tvoja ochrana neucinna, pretoze si na vystupe nic nerobil. Ja by som bol najradsej, keby sa filtrovalo na oboch stranach. Ten skript na mojom webe, ako som uz ostatne pisal, pochadza z mozilly, ja som vyzdvihoval len jeho ucinnost, ktoru ty popieras (to len preto, aby si si nemyslel, ze som si ho vycucal z prstu).

ikona Jakub Vrána OpenID:

To je pořád dokola. Já přece netvrdím, že je zbytečné ošetřovat všechny znaky. Tvrdím, že je potřeba ošetřovat <&"> a naopak zbytečné ošetřovat (+-%). Na tebou poskytnutý příklad samozřejmě toto pravidlo bohatě dostačuje.

Co se ošetřování vstupu a výstupu týče, opět každý mluvíme o něčem jiném. Samozřejmě, že funkci htmlspecialchars() je nutné volat na výstupu. Ověření validity UTF-8 je ale lepší dělat na vstupu. A znovu opakuji, že aplikace se nemusí spoléhat na to, že v databázi budou data validní. Důležité je, aby API, která tato data z databáze získává, validitu dat zaručilo. Takže i kdyby databáze za korektnost dat neručila (např. MySQL za ni s výjimkou blobů ručí), zaručí ji toto API.

Tohle platí obzvlášť u velkých projektů - data z databáze prostě nejdou získat jinak než přes API, které zajistí jejich validitu. Argumentovat tím, že by to programátor mohl obejít, je totéž jako argumentovat tím, že by mohl obejít výstupní ošetření.

Soustřeď se prosím na to, co je napsané v článku a co jsem napsal v diskusi. Vyvracíš argumenty, které nikdy nepadly, a naopak ignoruješ ty vznesené.

Ochrana na addons.mozilla.org je převzata z CakePHP. Zbytečná je v obou projektech. Není to chyba v tom smyslu, že by způsobila nějaký problém, pouze to je zbytečná práce, která by se nemusela dělat.

ikona Rastislav Turek:

Ano, na priklad ktory som ukazal pravidlo postacuje. Je vsak hodne zlozite prechadzat weby aby som nasiel priklad, ktory pravidlu nevyhovuje. Nestaci ti predstavivost? Plus IE dodnes bez problemov spusti encodovanu url, teda nieco ako HREF="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D" samozrejme si to upravis podla situacie. Mal som aj dva priklady, ale uz ich po reporte opravili.

No za prve MySQL nie je jedina databaza s ktorou pracujes, za druhe, nemusi byt vzdy dopre nastavena. Rovnako samotne API moze byt zmenene, resp. nan nemusis mat priamy dosah, dovody si vymyslat nejdem. Ja som za to, aby boli do db vkladane uz ciste data, pekne a upravene, ale ak sa ti dostane nieco dovnutra potom, tak sa utok podari, kedze si odfiltroval len vstup. Predstav si, ze ti do db, ktora vracia napriklad nazov linky niekto vlozi prave encodovany string, ktory sa ti zapise do href. Ani jedna z ochran nezafunguje, kedze pre vsetky to vyzera ako spravny retazec. Moznosti je samozrejme ovela viac.

Ak je zbytocna ochrana v obocho projektoch, tak im to prosim napis. Som zvedavy na ich reakciu. Snad tolko pre mna mozes urobit.

ikona Jakub Vrána OpenID:

Jak už jsem psal u tebe na blogu, nemusíš hledat praktický příklad, stačí teoretická ukázka, Proof of Concept. http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D tím příkladem opět není - když % převedeš na &#37; tak odkaz zůstane odkazem.

Nepodsouvej mi prosím zase něco, co jsem neřekl. Nikdy jsem argumenty nekonkretizoval na MySQL, jen jsem ho uvedl jako ukázku toho, že se o UTF-8 validaci dat může starat sama databáze. Pokud to nedělá, musí se o to starat API, přes které se s databází komunikuje. Snažit se to dohánět až na výstupu z aplikace je pozdě, protože vnitřek aplikace by pracoval s nevalidními daty (nefungovala by např. ani funkce pro zjištění délky řetězce).

V zavedeném projektu není šance, že by odstranili ochranu, která se tváří jako bezpečnostní, pokud nezpůsobuje nějakou chybu. A to ošetřování znaků (+-%) nezpůsobuje, jen je zbytečné. Jediná možnost je přesvědčit původního autora o tom, že se spletl. O to jsem se pokusil už minulý týden, jak jsem psal u tebe na blogu.

ikona Rastislav Turek:

Ako som uz pisal u seba, mas pravdu. Snazil som sa najst racionalne vysvetlenie preco prevadzat aj tie dalsie znaky na entity, no ochranou to skutocne nie je, resp. staci prevadzat len znaky <>"' zvysok uz zafunguje ako ochrana len velmi vynimocne.

Ak vsak nevies ovplyvnit ani databazu a ani API pre komunikaciu, musis si ochranu riesit na vystupe. Preto je vhodnejsie sa vzdy takto postavit k ochrane aj za cenu, ze by ti to malo rovno spomalit o malinko pracu skriptu, pretoze sa nemozes vzdy spoliehat na casti, ku ktorym nemas pristup. Ale inak sa zhodneme, aj ked to chvilku trvalo ;)

ikona Jakub Vrána OpenID:

Uf, to jsem si oddechl. Pro přesnost uvedu, že ošetřit je potřeba <&> a uvnitř hodnot atributů uzavřených do uvozovek nebo apostrofů ještě tyto (zapomněl jsi na ampersand).

Co se vstupního a výstupního ošetřování týče, už to nemusíme dál rozebírat. Já trvám na tom, že uvnitř aplikace by měla být data UTF-8 validní a snažit se je takto zvalidnit až na výstupu je pozdě. Tam už to prostě nedohoníš - vyřešíš sice jeden možný důsledek problému, ale nevyřešíš jeho příčinu.

ikona david@grudl.com:

Gratuluji :-)))

pojízdná kočka:

překlep v článku:
neuzavřená hodnota atribitu -> atributu

ikona Jakub Vrána OpenID:

Díky za upozornění, opravil jsem to.

budulinek:

co presne znamena: Pozor je potřeba si dát taky na vkládání externích URL (odkazy a obrázky). ?

jak by melo vypadat spravne osetreni uzivatelskeho vstup v <a href="...vstup..."> ?

viz napriklad policko url v tomto formulari :)

ikona Jakub Vrána OpenID:

Odkazy by neměly začínat na javascript:, rozumné je asi omezení na http: a https:. Pro obrázky platí totéž, pokud chci navíc zamezit sledování přístupů (nebo mám v URL tajné informace, to by být ale neměly), tak je potřeba to omezit na důvěryhodné servery.

ikona v6ak:

Asi tak, jsem proti blacklistu v podobě zákazu "javascript:" na začátku, mám radši whitelist. Stačí zvážit tyto možnosti:

* Co když na začátek někdo přidá whitespace?
* Co když to bude s různou velikostí písmen?
* Co když bude obsahovat jinou úpravu (myslím, že jsem někde viděl celkem drastické úpravy)?
* A HLAVNĚ: Je "javascript:" zaručeně jedinou nebezpečnou možností, nebo může v budoucnu přibýt "python:" apod.? Možná IE již dnes umí "vbscript:". Tady si IMHO nemůžeme být kdykoli jisti.

Takže bych řekl, že whitelist je dokonce jediné správné řešení.

Petr F:

Tady v RULE #1 zmiňují, že je nutné escapovat i lomítko. Co myslíte, je to nutné?

https://www.owasp.org/index.php/XSS_%28Cross_…_Element_Content

ikona Jakub Vrána OpenID:

Ne, je to zbytečné.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.