What's new in Latte 2.8: fortifications inside the template

4 years ago by David Grudl  

Latte 2.8 has an armored stronghold directly under the hood. This is an important feature that protects applications that use templates from untrusted sources. For example, when they are edited by the users themselves. How to build fortifications inside templates?

When someone asks me what are the three killer features of Latte, I answer:

  1. Latte is best secured against XSS
  2. uses PHP syntax so I don't have to learn another language
  3. have sexy n:attributes and optional chaining {$user?->address?->street}

Templates for PHP surprisingly often use Python syntax (such as Twig, Volt, etc.), which not only requires you to learn a new language, but then constantly switch between it and PHP. One of the languages uses foreach ($people as $person), the second one for person in people, and it is too easy to make mistakes.

What a convenience that we use good old PHP in Latte!

Latte used to be completely benevolent to what is written with PHP in it, what functions were called, etc. This has changed with version 2.8, which comes with a so-called sandbox mode that makes sure that the sand does not get out of the box. Thus, it provides limited access to macros, filters, functions, methods, etc. This allows the use of templates from untrusted third parties, such as end users who edit them themselves.

How does it work?

We simply define what we want to allow in the template. In the beginning, everything is forbidden and we gradually grant permissions:

$policy = new Latte\Sandbox\SecurityPolicy;
$policy->allowMacros(['block', 'if', 'else', '=']);
$policy->allowFilters($policy::ALL);

// and pass to Latte
$latte->setPolicy($policy);

// inside of presenter:
// $this->template->getLatte()->setPolicy($policy);

This code allows the author of the template to use the {block}, {if}, {else} and {=} tags. The latter is printing {=$var}, but also a more commonly used shorter form {$var}. Furthermore, we have enabled all filters so that the user will be able to use {$var|upper}.

However, we did not allow any functions, methods, or properties of the objects, so calling {=trim($var)} or {$user->isLoggedIn()} will lead to exception Latte\SecurityViolationException. You can enable them as follows:

$policy->allowFunctions(['trim', 'strlen']);
$policy->allowMethods(Nette\Security\User::class, ['isLoggedIn', 'isAllowed']);
$policy->allowProperties(Nette\Database\Row::class, $policy::ALL);

As you can see, we have allowed access to all properties for object Nette\Database\Row.

Isn't that amazing? You can control everything at a very low level.

Creating policies from scratch, when everything is forbidden, may not be convenient, so you can start from a safe foundation:

$policy = Latte\Sandbox\SecurityPolicy::createSafePolicy();

This means that all standard macros are allowed except for contentType, debugbreak, dump, extends, import, include, includeblock, layout, php, sandbox, snippet, snippetArea, templatePrint, varPrint, widget. All standard filters are allowed as well except for datastream, noescape and nocheck. Finally, access to the methods and properties of object $iterator is allowed too.

It is also not possible to access $this and use PHP keywords (e.g. throw, new, echo,…) or the backtick. operator in a sandboxed template.

The last important thing remains to be said: the rules apply to the template that we insert with the new {sandbox} tag. Which is a something like {include}, but it turns on sandbox mode and also doesn't pass any external variables.

{sandbox untrusted.latte}

Thus, the layout and individual pages can use all macros and variables as before, restrictions will be applied only to the template untrusted.latte. Variables are passed as follows:

{sandbox untrusted.latte, title => $article->title, logged => $user->isLoggedIn()}

Some violations, such as the use of a forbidden tag or filter, are detected at compile time. Others, such as calling unallowed methods of an object, at runtime. The template can also contain any other bugs. In order to prevent an exception from throwing from the sandboxed template, which disrupts the entire rendering, you can define your own exception handler, which, for example, just logs it:

$latte->setExceptionHandler(function (Throwable $e, Latte\Runtime\Template $template) use ($logger) {
	$logger->log($e);
});

If we want to turn on sandbox mode directly for all templates, it's easy:

$latte->setSandboxMode();