What's new in Latte 2.8: fortifications inside the template
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:
- Latte is best secured against XSS
- uses PHP syntax so I don't have to learn another language
- 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();
Sign in to submit a comment