Sandbox
*******
.[perex]
Sandbox poskytuje bezpečnostní vrstvu, která vám dává kontrolu nad tím, jaké značky, PHP funkce, metody apod. mohou být v šablonách použity. Díky sandbox režimu můžete bezpečně spolupracovat s klientem nebo externím kodérem na tvorbě šablon, aniž byste se museli obávat, že dojde k narušení aplikace nebo nežádoucím operacím.
Jak to funguje? Jednoduše nadefinujeme, co všechno šabloně dovolíme, přičemž v základu je všechno zakázané a my postupně povolujeme. Následujícím kódem umožníme autorovi šablony používat značky `{block}`, `{if}`, `{else}` a `{=}`, což je značka pro [vypsání proměnné nebo výrazu |tags#Vypisování] a všechny filtry:
```php
$policy = new Latte\Sandbox\SecurityPolicy;
$policy->allowTags(['block', 'if', 'else', '=']);
$policy->allowFilters($policy::All);
$latte->setPolicy($policy);
```
Dále můžeme povolit přístup k jednotlivým globálním funkcím, metodám nebo properties objektů:
```php
$policy->allowFunctions(['trim', 'strlen']);
$policy->allowMethods(Nette\Security\User::class, ['isLoggedIn', 'isAllowed']);
$policy->allowProperties(Nette\Database\Row::class, $policy::All);
```
Oprávnění udělená přes `allowMethods()` a `allowProperties()` platí i pro instance potomků dané třídy (kontrola používá `is_a()`). Názvy metod nerozlišují velká a malá písmena, stejně jako v PHP, názvy properties je naopak rozlišují.
Není to úžasné? Můžete na velmi nízké úrovni kontrolovat úplně všechno. Pokud se šablona pokusí zavolat nepovolenou funkci nebo přistoupit k nepovolené metodě nebo property, skončí to výjimkou `Latte\SecurityViolationException`.
Bezpečná výchozí policy
=======================
Tvořit policy od nuly, kdy je zakázáno úplně vše, nemusí být pohodlné, proto můžete začít od bezpečného základu:
```php
$policy = Latte\Sandbox\SecurityPolicy::createSafePolicy();
```
Bezpečný základ znamená, že jsou povoleny všechny standardní tagy kromě `contentType`, `debugbreak`, `dump`, `extends`, `import`, `include`, `layout`, `php`, `sandbox`, `snippet`, `snippetArea`, `templatePrint`, `varPrint`, `embed`. Jsou povoleny standardní filtry kromě `datastream`, `noescape` a `nocheck`. A nakonec je povolený přístup k metodám a properties objektu `$iterator`.
Vlastní policy .{data-version:3.1.6}
====================================
Místo `SecurityPolicy` můžete metodě `setPolicy()` předat i vlastní implementaci rozhraní `Latte\Policy`. Počítejte ale s tím, že značky, filtry a funkce se kontrolují už při kompilaci šablony a zkompilovaná šablona se pak ukládá do cache. Aby dvě různě nastavené instance vaší třídy nesdílely tutéž zkompilovanou šablonu, doplňte metodu `getCacheSignature()`, která vrátí hodnotu popisující pravidla, třeba jejich pole:
```php
class MyPolicy implements Latte\Policy
{
public function __construct(
private array $functions,
) {
}
public function getCacheSignature(): mixed
{
return $this->functions;
}
// isTagAllowed(), isFunctionAllowed() atd.
}
```
Pokud metoda chybí, Latte vás na to upozorní deprecation hláškou.
Aktivace sandboxu
=================
Pravidla se aplikují na šablonu, kterou vložíme značkou [`{sandbox}` |tags#Vložení šablony]. Což je jakási obdoba `{include}`: zapíná sandbox režim a stejně jako `{include}` automaticky nepředává okolní proměnné. Předat je ale můžete explicitně, například `{sandbox 'untrusted.latte', a: 1, b: 2}`:
```latte
{sandbox 'untrusted.latte'}
```
Tedy layout a jednotlivé stránky mohou nerušeně využívat všechny tagy a proměnné, pouze na šablonu `untrusted.latte` budou uplatněny restrikce.
Některé prohřešky, jako použití zakázaného tagu nebo filtru, se odhalí v době kompilace. Jiné, jako třeba volání nepovolených metod objektu, až za běhu. Šablona také může obsahovat jakékoliv jiné chyby. Aby vám ze sandboxované šablony nemohla vyskočit výjimka, která naruší celé vykreslování, lze definovat vlastní [handler pro výjimky |develop#Exception handler], který ji třeba zaloguje.
Pokud bychom chtěli sandbox režim zapnout přímo pro všechny šablony, jde to snadno:
```php
$latte->setSandboxMode();
```
Kontrola vygenerovaného kódu
============================
Abyste měli jistotu, že uživatel do stránky nevloží PHP kód, který je sice syntakticky správný, ale zakázaný a způsobí PHP Compile Error, doporučujeme nechávat [šablony kontrolovat PHP linterem |develop#Kontrola vygenerovaného kódu]. Tuto funkčnost zapnete metodou `Engine::enablePhpLinter()`. Jelikož ke kontrole potřebuje volat binárku PHP, cestu k ní předejte jako parametr:
```php
$latte = new Latte\Engine;
$latte->enablePhpLinter('/path/to/php');
```
Co sandbox nehlídá
==================
Nad rámec značek, funkcí, metod a properties, které povolíte, sandbox bez ohledu na policy vždy zakazuje několik konstrukcí: operátor `new`, proměnnou `$this`, variable variables (`$$var`) a filtr `|noescape`.
Hlídané je i čtení všech properties objektu najednou. Přetypování objektu na pole pomocí `(array)` je zakázané vždy, protože by prozradilo i jeho privátní properties. A `{foreach}` přes obyčejný objekt (tedy takový, který neimplementuje `Traversable`) projde, jen pokud policy povoluje všechny jeho veřejné properties.
Sandbox spolehlivě hlídá *explicitní* operace: volání funkcí, metod a filtrů i přístup k properties objektů. Na jednu věc ale jeho kontrola nedosáhne a je dobré o ní vědět.
Když objekt vypíšete nebo ho jakkoliv použijete v řetězcovém kontextu, PHP automaticky zavolá jeho magickou metodu `__toString()`. Tento *implicitní* převod policy nekontroluje, takže proběhne, i kdyby metoda `__toString()` nebyla mezi povolenými. Autor šablony tak může spustit `__toString()` libovolného objektu, na který v šabloně dosáhne. To je rozdíl oproti explicitnímu zápisu `{$obj->__toString()}`, který sandbox zablokuje:
```latte
{$obj} {* zavolá __toString() *}
{$obj . '!'} {* totéž (konkatenace) *}
{="cena: $obj"} {* totéž (interpolace v řetězci) *}
{$obj|upper} {* totéž (přes filtr) *}
```
Smyslem metody `__toString()` je vyrobit textovou reprezentaci objektu, takže její dosažitelnost obvykle nevadí. Problém nastává jen tehdy, když má `__toString()` vedlejší efekty (třeba zápis nebo dotaz do databáze) nebo když vrací citlivá data.
Tyhle nejpřímější případy by Latte zachytit dokázalo, ale ne spolehlivě a ne všechny. Převod objektu na řetězec totiž není volání metody, ale vestavěná operace jazyka, ke které dochází na mnoha místech výrazu. Někde (třeba při porovnání objektu s řetězcem nebo uvnitř volané funkce či filtru) by ji nešlo spolehlivě zachytit, aniž by se přitom blokovalo i legitimní použití. Proto je s dosažitelností `__toString()` potřeba počítat.
**Nevystavujte sandboxu objekty, jejichž `__toString()` má vedlejší efekty nebo zpřístupňuje citlivé údaje.** A to nejen ty, které šabloně předáte přímo, ale i ty, které autor získá jako návratovou hodnotu povolené funkce, metody nebo property.
{{maintitle: Sandbox v Latte: bezpečné spouštění cizích šablon}}
{{description: Určete, jaké tagy, filtry, PHP funkce a metody smí šablona používat. Klient nebo externí kodér pak může šablony upravovat, aniž by ohrozil vaši aplikaci.}}
Sandbox
Sandbox poskytuje bezpečnostní vrstvu, která vám dává kontrolu nad tím, jaké značky, PHP funkce, metody
apod. mohou být v šablonách použity. Díky sandbox režimu můžete bezpečně spolupracovat s klientem nebo externím
kodérem na tvorbě šablon, aniž byste se museli obávat, že dojde k narušení aplikace nebo nežádoucím operacím.
Jak to funguje? Jednoduše nadefinujeme, co všechno šabloně dovolíme, přičemž v základu je všechno zakázané a my
postupně povolujeme. Následujícím kódem umožníme autorovi šablony používat značky {block},
{if}, {else} a {=}, což je značka pro vypsání proměnné nebo výrazu a všechny filtry:
$policy = new Latte\Sandbox\SecurityPolicy;
$policy->allowTags(['block', 'if', 'else', '=']);
$policy->allowFilters($policy::All);
$latte->setPolicy($policy);
Dále můžeme povolit přístup k jednotlivým globálním funkcím, metodám nebo properties objektů:
$policy->allowFunctions(['trim', 'strlen']);
$policy->allowMethods(Nette\Security\User::class, ['isLoggedIn', 'isAllowed']);
$policy->allowProperties(Nette\Database\Row::class, $policy::All);
Oprávnění udělená přes allowMethods() a allowProperties() platí i pro instance potomků dané
třídy (kontrola používá is_a()). Názvy metod nerozlišují velká a malá písmena, stejně jako v PHP, názvy
properties je naopak rozlišují.
Není to úžasné? Můžete na velmi nízké úrovni kontrolovat úplně všechno. Pokud se šablona pokusí zavolat
nepovolenou funkci nebo přistoupit k nepovolené metodě nebo property, skončí to výjimkou
Latte\SecurityViolationException.
Bezpečná výchozí policy
Tvořit policy od nuly, kdy je zakázáno úplně vše, nemusí být pohodlné, proto můžete začít od bezpečného
základu:
$policy = Latte\Sandbox\SecurityPolicy::createSafePolicy();
Bezpečný základ znamená, že jsou povoleny všechny standardní tagy kromě contentType,
debugbreak, dump, extends, import, include, layout,
php, sandbox, snippet, snippetArea, templatePrint,
varPrint, embed. Jsou povoleny standardní filtry kromě datastream, noescape
a nocheck. A nakonec je povolený přístup k metodám a properties objektu $iterator.
Vlastní policy
Místo SecurityPolicy můžete metodě setPolicy() předat i vlastní implementaci rozhraní
Latte\Policy. Počítejte ale s tím, že značky, filtry a funkce se kontrolují už při kompilaci šablony a
zkompilovaná šablona se pak ukládá do cache. Aby dvě různě nastavené instance vaší třídy nesdílely tutéž
zkompilovanou šablonu, doplňte metodu getCacheSignature(), která vrátí hodnotu popisující pravidla, třeba
jejich pole:
class MyPolicy implements Latte\Policy
{
public function __construct(
private array $functions,
) {
}
public function getCacheSignature(): mixed
{
return $this->functions;
}
// isTagAllowed(), isFunctionAllowed() atd.
}
Pokud metoda chybí, Latte vás na to upozorní deprecation hláškou.
Aktivace sandboxu
Pravidla se aplikují na šablonu, kterou vložíme značkou {sandbox}. Což je jakási obdoba
{include}: zapíná sandbox režim a stejně jako {include} automaticky nepředává okolní proměnné.
Předat je ale můžete explicitně, například {sandbox 'untrusted.latte', a: 1, b: 2}:
{sandbox 'untrusted.latte'}
Tedy layout a jednotlivé stránky mohou nerušeně využívat všechny tagy a proměnné, pouze na šablonu
untrusted.latte budou uplatněny restrikce.
Některé prohřešky, jako použití zakázaného tagu nebo filtru, se odhalí v době kompilace. Jiné, jako třeba volání
nepovolených metod objektu, až za běhu. Šablona také může obsahovat jakékoliv jiné chyby. Aby vám ze sandboxované
šablony nemohla vyskočit výjimka, která naruší celé vykreslování, lze definovat vlastní handler pro výjimky, který ji třeba zaloguje.
Pokud bychom chtěli sandbox režim zapnout přímo pro všechny šablony, jde to snadno:
$latte->setSandboxMode();
Kontrola vygenerovaného kódu
Abyste měli jistotu, že uživatel do stránky nevloží PHP kód, který je sice syntakticky správný, ale zakázaný a
způsobí PHP Compile Error, doporučujeme nechávat šablony kontrolovat PHP linterem. Tuto funkčnost
zapnete metodou Engine::enablePhpLinter(). Jelikož ke kontrole potřebuje volat binárku PHP, cestu k ní předejte
jako parametr:
$latte = new Latte\Engine;
$latte->enablePhpLinter('/path/to/php');
Co sandbox nehlídá
Nad rámec značek, funkcí, metod a properties, které povolíte, sandbox bez ohledu na policy vždy zakazuje několik
konstrukcí: operátor new, proměnnou $this, variable variables ($$var) a filtr
|noescape.
Hlídané je i čtení všech properties objektu najednou. Přetypování objektu na pole pomocí (array) je
zakázané vždy, protože by prozradilo i jeho privátní properties. A {foreach} přes obyčejný objekt (tedy
takový, který neimplementuje Traversable) projde, jen pokud policy povoluje všechny jeho veřejné properties.
Sandbox spolehlivě hlídá explicitní operace: volání funkcí, metod a filtrů i přístup k properties objektů.
Na jednu věc ale jeho kontrola nedosáhne a je dobré o ní vědět.
Když objekt vypíšete nebo ho jakkoliv použijete v řetězcovém kontextu, PHP automaticky zavolá jeho magickou metodu
__toString(). Tento implicitní převod policy nekontroluje, takže proběhne, i kdyby metoda
__toString() nebyla mezi povolenými. Autor šablony tak může spustit __toString() libovolného
objektu, na který v šabloně dosáhne. To je rozdíl oproti explicitnímu zápisu {$obj->__toString()}, který
sandbox zablokuje:
{$obj} {* zavolá __toString() *}
{$obj . '!'} {* totéž (konkatenace) *}
{="cena: $obj"} {* totéž (interpolace v řetězci) *}
{$obj|upper} {* totéž (přes filtr) *}
Smyslem metody __toString() je vyrobit textovou reprezentaci objektu, takže její dosažitelnost obvykle nevadí.
Problém nastává jen tehdy, když má __toString() vedlejší efekty (třeba zápis nebo dotaz do databáze) nebo
když vrací citlivá data.
Tyhle nejpřímější případy by Latte zachytit dokázalo, ale ne spolehlivě a ne všechny. Převod objektu na řetězec
totiž není volání metody, ale vestavěná operace jazyka, ke které dochází na mnoha místech výrazu. Někde (třeba při
porovnání objektu s řetězcem nebo uvnitř volané funkce či filtru) by ji nešlo spolehlivě zachytit, aniž by se přitom
blokovalo i legitimní použití. Proto je s dosažitelností __toString() potřeba počítat.
Nevystavujte sandboxu objekty, jejichž __toString() má vedlejší efekty nebo zpřístupňuje citlivé
údaje. A to nejen ty, které šabloně předáte přímo, ale i ty, které autor získá jako návratovou hodnotu povolené
funkce, metody nebo property.