Checklist na zabezpečení webových aplikací

cs v kategorii code • 9 min. čtení
Mind the age! Most likely, its content is outdated. Especially if it’s technical.

V poslední době jsem častokrát řešil bezpečnost webů a vzpomínal, cože vše to musím mít v cajku. Tak jsem se rozhodl si sepsat takový check-list. A možná bude užitečný i někomu jinému. :-)

Základem je HTTPS

Bez toho asi nemá smysl ani řešit nic jiného. Takže to je must-have první věc, kterou řešit, pokud chcete web zabezpečit. A ne jen nutně zabezpečit, cokoliv proběhne po drátech nešifrovaně, může kdekoliv po cestě někdo upravit. Třeba se na nás přiživovat vkládáním vlastní reklamy. Jop, i tací poskytovatelé internetu mezi námi existují.

HSTS

Když už je HTTPS nastavené, další krok je mít správně nastavený redirect z HTTP na HTTPS. Aby se nestalo, že lze nějak web prohlížet přes nezabezpečené vody. Když už jste si dali tu práci, ať to má smysl.

Redirect však není vše. Uživatelé často pečou na to, co do prohlížeče zadají, a tak útočník může vylákat oběť na HTTP, i když třeba máme redirecty pořešené. Proto nastavíme HSTS hlavičku, aby prohlížeč ani HTTP nezkoušel a v takovém případě sám přesměroval. (Ano, znamená to, že když podělám SSL, nebude vůbec fungovat web. Ale mnohdy lepší žádný, než nebezpečný.)

Strict-Transport-Security: max-age=31536000; includeSubDomains

Session cookie

Web je nyní zabezpečený a snažíme se našeho uživatele držet od HTTP co nejdále. Ale nikdy si nemůžeme být jisti, co se útočníkovi povede udělat. Proto budeme držet to nejcenější v co největším bezpečí. Mluvím o session v cookie. Rozhodně by měla být nastavena jako HTTP, což znamená, že ji nelze přečíst JavaScriptem. Mít nastaveno taky Secure není taky na škodu. Znamená to, že cookina půjde pouze po šifrovaném spojení.

CSP

Neboli Content Security Policy je hlavička povolující jen určité zdroje. To znamená, že touto hlavičkou lze znemožnit útočníkovi vložit nebezpečný script. Resp. vložit může, ale nic to neudělá. Lze nastavit defaultně pro všechny, případně pro každý typ zvlášť (obrázek, fonty, JavaScripty, …).

Rozhodně je dobré se vyhnout unsafe-inline či dokonce unsafe-eval. Nejlépe dovolit jen vaši doménu, případně ještě ověřené CDNky. A samozřejmě nejen zakázat, ale také zakázané pokusy nahlásit – je dobré vědět, zda se někdo o něco pokouší.

Content-Security-Policy: default-src 'self'; report-uri /csp-report;

Případně lze využít verzi, která jen nebezpečný zdroj napráší. Vhodné do začátku, než se odladí již běžící web, ale rychle bych se dostal do striktního módu.

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;

XSS

CSP je fajn, ale pokud se útočníkovi povede dostat do stránky script přes naši samotnou webovou aplikaci, jsme namydlení. Naštěstí prohlížeče obsahují zabudovanou ochranu proti XSS (Cross Site Scripting). Normálně bývá zapnutá, ale jistota je jistota… (A opět lze bonzovat, což je asi to nejužitečnější na této hlavičce.)

X-Xss-Protection: 1; mode=block; report=/xss-report;

Iframe

Iframy se sice už nepoužívají, ale neměli bychom na ně zapomínat. Skrývají totiž potenciální nebezpečí. Útočník může naši stránku dát do iframe, překrýt nějakou zajímavou hrou (třeba klikací reklama v podobě „chytni všechny míče a máš možnost vyhrát nový iPhone“) a donutit tak uživatele kliknout na místa na našem webu, jak potřebuje. Proto je nejlepší úplně zablokovat možnost naši stránku v nějakém iframe mít.

X-Frame-Options: DENY

Typy souboru

Ať už na webu máme nahrávání souborů, které pak i zobrazujeme, či nikoliv, nikdy si nemůžeme být jisti, co se útočníkovi povede. Proto je možnost mu zase jednoduše znepříjemnit práci a to tak, že typ souboru budeme vždy určovat my a prohlížeč se nebude snažit hádat. Tedy pokud útočník nahraje JavaScript či celou HTML stránku, ale my takové věci nepodporujeme, pak prostě jako HTML stránku nevydáme a tudíž se nic nestane.

X-Content-Type-Options: nosniff

CSRF

Už jste si všimli, že e-mailové aplikace se ptají, zda zobrazit obrázky? Je to z důvodu, že pokud jste například přihlášeni na Facebooku a útočník vám poslal e-mail s obrázkem, který není obrázek, ale jen odkaz na smazání účtu, a vy se na takový obrázek chcete podívat… máte po účtu. Teoreticky. Bude to platit na webech, kde takové akce lze provést obyčejným GET požadavkem, což Facebook není.

Základem je veškeré akce nedělat GETem, ale POSTem či ostatními. To ale nezabrání místo obrázku využít například formulář, který uživatel vyplní nevědomky, neb si myslí, že je o něčem úplně jiném. Proto je dobré přidat ke každému požadavku ještě nějaký token. Na serveru vygenerovat token a ten při akci poslat zpět (a zvalidaovat, samozřejmě). Poté útočník musí nejprve zjistit i validní CSRF token, aby mohl takto uškodit. Což je podstatně složitější, neb web máme za SSL.

HPKP

Třešničkou na dortu je HPKP, což určuje, kterým certifikátům věřit. Defaultně prohlížeče věří všem certifikátům autorit, které mají v seznamu věrohodných. Jenže už se stalo, že byla certifikační autorita napadnuta…

Public-Key-Pins: pin-sha256='…'; pin-sha256='…'; max-age=5184000; report-uri=/hpkp-report;

V ukázce je vidět dvakrát pin-sha256. To není překlep, ale první je aktuálně používaný a druhý backup. Například příprava na vyexpirování atp. A opět lze zároveň bonzovat pokusy a nebo pouze bonzovat.

Public-Key-Pins-Report-Only: pin-sha256='…'; pin-sha256='…'; max-age=5184000; report-uri=/hpkp-report;

Hesla

Pokud se spravují hesla, musí se sledovat novinky a mít hesla co nejlépe zabezpečená. Tedy žádné MD5 hashe, ale co nejpomalejší hashovací algoritmus. Jakýkoliv algoritmus je oslaben proti jednomu typu útoku – brute force. Proto je dobré vybrat pomalý algoritmus, aby si nemohl útočník louskat hashe na jeho Raspberry po večerech. Často jsem používal PBKDF2 (default v Djangu), ale nic se nezkazí s bcrypt (náročné na CPU) či scrypt (náročné na paměť), které už jsou rozšířené. Mimochodem výkon lze stále lépe škálovat, než paměť.

Jelikož uniknutí databáze, kterou by mohl začít někdo louskat, nehrozí tak často, je potřeba se zamyslet taky nad přihlašovacím formulářem. Není dobré nechat útočníka v pohodě zkoušet louskat přímo na našem webu. Minimálně si monitorovat nebezpečné množství pokusů, raději s automatickým zpomalováním až blokováním. S blokováním však opatrně, neb se pak může zamezit přístup skutečným uživatelům. Například u naší „interní“ aplikaci víme, odkud se uživatelé přihlašují, takže můžeme podezřelá místa rovnou blokovat. U aplikací pro širokou veřejnost je lepší volbou zpomalování.

Logování

Pokud máte vše až potud, web máte dobře zabezpečen. :-) Ale dovolím si na závěr ještě jednu drobnost. A to, že i na druhé straně, tedy na vašem serveru, je potřeba k citlivým údajům přistupovat opatrně. Základem je mít vypnut debug mód v produkci (a nejlépe i na testu). I když má například Werkzeug debugger nově PIN k aktivaci, je to prostě díra.

Co je ale důležitější, je logování. Logujete si celé požadavky i se vstupními parametry? Všude, včetně login obrazovky? Pak máte logy pravděpodobně plné hesel v čitelné podobě. My jsme si například ve Flasku přetížili Werkzeugový ImmutableOrderedMultiDict, který skryje nebezpečné hodnoty.

Další tip, pokud pracujete v kódu často s citlivými údaji, použít nějaký speciální objekt, který nedovolí nijak zobrazit vnitřní hodnotu. Můžete pak hodnotu přenášet všude možně a máte jistotu, že se po cestě nezaloguje. Něco ve stylu:

class SecretValue(object):
    def __init__(self, secret_value):
        self.__secret_value = secret_value

    def get_secret_value(self):
        return self.__secret_value

    def __str__(self):
        return '--secret-value--'

    def __repr__(self):
        return '<SecretValue>'

>>> v = SecretValue('aaa')
>>> str(v)
'--secret-value--'
>>> repr(v)
'<SecretValue>'
>>> str({'value': v})
"{'value': }"
>>> logging.info(p)
INFO:root:--secret-value--
>>> logging.info('value: {}'.format(v))
INFO:root:value: --secret-value--

Poslední tip je na tracebacky. Python to například nedělá, ale PHP nahradí parametry funkcí za skutečné hodnoty. Docela užitečná věc pro debugování, ale nepraktická na citlivé údaje. Doporučuji takové chování vypnout, pokud lze. Pokud nelze, vybrat si rozumější nástroj. :-)


A to je vše! Aspoň z toho nejvíce důležitého. Pokud si myslíte, že mi něco ještě chybí, klidně mne upozorněte v komentářích.






11 reakcí

U HSTS existuje ještě parametr preload. Přes https://hstspreload.appspot.com/ jde dostat url do kódu Chrome, Firefox, Edge a prohlížeč pak použije přímo https.

Iframe se používají v řadě analytických nástrojů včetně google analytics. Striktním zákazem vložení do rámce si tyto nástroje zabijeme. Pokud se toto stane v korporaci, může probublání od jednoho analytického oddělení k druhému security oddělení a jejich dohoda trvat měsíce až roky.

O to smutnější je pak fakt, že může jít o zcela statický web, jehož uzavření do rámce nemůže nadělat žádnou škodu.

@Chemická tabulka Díky za doplnění, jak se zbavit první návštěvy po HTTP. Jen ten list začíná být velký, snad brzy použijí bloom filter. :-)

@Martin U statického webu samozřejmě neplatí spoustu zmíněných bodů. Ale zbytečně bych nepovoloval něco, co není potřeba. Konkrétně s iframe lze nastavit, na kterých webech je povoleno s ALLOW-FROM, viz https://tools.ietf.org/html/rfc7034#page-8

Super článek. Pro kompletnost bych k těmto základním technikám přidal:

- obranu proti SQL injection

- solení hesel kvůli předpočítaným tabulkám

@Martin U popisu iframe nejde o to, že by snad ve stránce nešly vytvářet iframes, ale o to, že stránka, která pošle hlavičku X-Frame-Options: deny nelze do iframe vložit – a takto analytické nástroje nefungují. Ty, pokud iframe potřebují, tak jej vytvoří ve stránce a do něj načtou "3rd party" obsah. Do iframe nenačítají měřenou stránku. Standardní měření pomocí GA dokonce ani iframe nevytváří a funguje i na stránkách, které zmíněnou hlavičku X-Frame-Options: deny posílají.

@Václav Makeš Solení hesel je něco, co by programátor aplikace neměl řešit. To má řešit automaticky knihovna na hashování hesel (v PHP to třeba password_hash() dělá), protože když to bude dělat programátor, který detailně nezná použité algoritmy, tak celé hashování může poslat do kytek. Třeba jako nápad "slyšeli jsme něco o saltu, tak heslo prefixujeme statickým saltem 56 znaků dlouhým a pak zahashujeme bcryptem, protože salt, že jo, dyť co se může stát"... viz https://github.com/PrestaShop/PrestaShop/pull/4609

@Václav Makeš, @Michal Špaček: Přesně tak, solení jsem nezmiňoval, neb zmíněné hashovací algoritmy již solení obsahují v sobě. Podobně jsem nezmiňoval SQL injection, protože to je další věc, kterou by programátor neměl řešit. Mělo by to být zabudované automaticky ve frameworku, ať už je jakýkoliv.

já bych jen dodal, že prvních 9 bodů by ani samotná aplikace (tj. programátoři) řešit neměla a měla by to být na starosti webserver/proxy.

--

Máte jednu aplikace, je to dobrý nápad vše nacpat do ní, máte desítky aplikací a postupně je aktualizujete, po pár iteracích se nějaká nastavení mohou ztratit a díra je na světě. Naopak na jednom místě konfigurovaná proxy se dá lehčejí kontrolovat a testovat a všechny aplikace mají stejnou základní úroveň zabezpečení...

@Tomáš: To záleží. Každá aplikace je jiná a bezmyšlenkovitě něco automaticky nastavovat mi nepřijde dobré. Mimochodem v proxy lze nastavit pouze hlavičky, nic víc tam udělat nelze.

@Michal: ano, ale větina těch doporučení je o hlavičkách :)

Pokud máš jednu aplikaci nebo prototyp, měj to kde chceš, ale u větších aglomerací je nutné bezpečnost řešit společně pro celou infrastrukturu a ne viset na programátořích každé aplikace, zejména pokud jde o tady doporučené věci, které jsou všude společné.

Zároveň tím neduplikuješ logiku, kdy musíš u každé aplikace přes běžný vývojový cyklus spravovat tyhle bezpečnostní hlavičky, ale máš je pod kontrolou na jednom místě.

Nikdo neřiká, že nemůže mít každá aplikace jiné nastavení, ale v konfiguraci je na jednom místě pro všechny.

Krásně to jde vidět u Public-Key-Pins, kdy často ani samotná aplikace nemá přístupné svoj ssl klíče, které jsou součástí nějakého balanceru, proxy. Stejně tak /*-report se musí spravovat mimo samotnou aplikaci hromadně, nikoliv, aby si je řešila každá aplikace zvlášť, zároveň je nesmyslné, aby tuhle hlavičku nastavovala aplikace a url spravoval někdo jiný.

Nechci tady zanášet nějaký spam nebo trolling, tvoje doporučení jsou super, tohle je jen můj pohled a moje zkušenosti.

@Tomáš: V pohodě, to není trolling. :-) Je pravda, že i když napsání těch /*-report adres je děsně primitivní, mohlo by to být na jednom místě. Zkusím tady navrhnout bezpečákům, aby udělali jednotnou URL pro všechny. :-)





Může se vám také líbit

en Makefile with Python, November 6, 2017
en Fast JSON Schema for Python, October 1, 2018
en Deployment of Python Apps, August 15, 2018
cs Jasně, umím Git…, August 6, 2014
cs Pokročilé regulární výrazy, August 17, 2014

Další články z kategorie code.
Nenechte si ujít nové články díky Atom/RSS kanálu.



Poslední příspěvky

cs Mami, tati, přejde to, December 9, 2023 in family
cs Co vše bychom měli dělat s dětmi?, November 24, 2023 in family
cs O trávicí trubici, November 7, 2023 in family
cs Na šestinedělí se nevyspíš, October 28, 2023 in family
cs Copak to bude?, October 20, 2023 in family