Writing Safe Templates

Matt Basta edited this page Dec 2, 2015 · 5 revisions

Safe templating is essential to any large web application. Safety comes from proper output sanitization in order to protect your site from cross-site scripting attacks. This is universal to all web applications, not just PHP or Brainy users.

For instance, consider this (broken) template:

{* This is an example of what NOT to do. Do not use! *}
<div class="user">
  Welcome back, {$user.name}!
</div>

If the user's name contains HTML characters, this poses a risk:

$user = [
  'name' => '<script>alert("foo")</script>',
];
$brainy->assign('user', $user);

When this code is run, it will result in the following:

<div class="user">
  Welcome back, <script>alert("foo")</script>!
</div>

Escaping

To resolve this, each variable that is included in a template must be properly escaped. The performance cost of escaping is very low, and the benefit derived from escaping everywhere possible is very high.

Brainy includes the |escape modifier, which provides a straightforward way to sanitize a variable's output:

<div class="user">
  Welcome back, {$user.name|escape}!
</div>

Escaping the output produces the following (correct) output:

<div class="user">
  Welcome back, &lt;script&gt;alert(&quot;foo&quot;)&lt;/script&gt;!
</div>

This result is safe, and does not pose a XSS risk.

Handling Already-Sanitized Values

Sometimes, your PHP code generates rendered markup for you. For example, you might have a library that processes Markdown into HTML, or a script that converts at-mentions into links. While in some circumstances it may be possible to write a Brainy plugin to allow this transformation to take place in the template layer, this is not always possible.

In order to facilitate this, the |unsafe_noescape modifier is provided. In other languages, this is known as the safe function, or requires extra syntax (Handlebars uses triple braces).

|unsafe_noescape is simply a pass-through: it tells Brainy that you understand the risk of outputting unsanitized markup directly to a template.

For example:

// It is assumed that our MarkdownLibrary sanitizes the input.
$renderedMarkup = MarkdownLibrary::convert($inputMarkdown);
$brainy->assign('content', $renderedMarkup);
<div class="rendered-content">
  {* @NOESCAPE: This value contains rendered markup that is generated by the PHP library *}
  {$content|unsafe_noescape}
</div>

When used in conjunction with expression modifier enforcement, this can help ensure that no unsanitized data is ever sent to a user. At Box, users of the unsafe_noescape modifier are required to leave a comment in the form {* @NOESCAPE: ... *} describing why unsafe_noescape is being used. This helps to ensure developers do not inadvertently misunderstand the use of unsafe_noescape on that line in the future.

Configuring Brainy

Brainy's default configuration requires all user-defined content to be escaped by default, or to use |unsafe_noescape. This is configurable with Smarty::$enforce_expression_modifiers. This value is an array of strings, where each string is the name of a any function or modifier/modifier compiler plugin that can be used to sanitize data. The default value is array('escape', 'unsafe_noescape'). When a user-supplied value is used without one of the listed sanitization functions, a BrainyModifierEnforcementException compiler exception will be raised.

To add support for other modifiers, you might specify something like this:

Smarty::$enforce_expression_modifiers = [
  'htmlspecialchars',
  'myCustomSanitizer',
  'my_custom_unsafe_noescape',
];

A Brainy installation with the above configuration would allow code that looks like this:

Some sanitary data: {$unsanitaryData|htmlspecialchars}<br>
Some more sanitary data: {$weirdUnsanitaryData|myCustomSanitizer}

Note that Brainy will require you to escape any value that may have passed through a function or plugin that caused side effects. For instance:

{$userValue|escape|upper} {* Will raise a compiler exception *}

Brainy does not know what the side effects of |upper are. It could, for instance, allow an attacker to format the input maliciously. Thus, Brainy requires this to be escaped again or to be marked with unsafe_noescape. The preferred approach is to only escape content that is formatted in its final form:

{$userValue|upper|escape}

In addition to Smarty::$enforce_expression_modifiers, Smarty::$enforce_modifiers_on_static_expressions is provided to help add additional security if needed. By default, Brainy is smart allow static expressions to render unaltered. For example:

{'This is a <b>static</b> expression'}
{3+4}

The above template will happily compile and run without requiring |escape or |unsafe_noescape, because no values are requested from user-supplied variables.

For developers that require extra security, you can set Smarty::$enforce_modifiers_on_static_expressions to true. This will require every expression to have one of the modifiers defined in Smarty::$enforce_expression_modifiers. This can be useful in applications that perform extensive magic or in systems handling exceptionally sensitive data.

Avoiding Sanitization Issues

It's a common practice in a Smarty template to capture output using the {capture} plugin (essentially a wrapper around ob_start() and ob_get_clean()). This can be dangerous because the escaped output is stored back into the same scope that inherently contains unescaped output. For example:

{capture assign="captured"}
  This data is <b>captured</b>.
{/capture}
{* In order to preserve the <b> tags, we need to use unsafe_noescape *}
{$captured|unsafe_noescape}

This can be risky, however. If the template becomes more complex, it can introduce vulnerabilities:

{if !$userSuppliedData}
  {capture assign="userSuppliedData"}
    This data is <b>captured</b>.
  {/capture}
{/if}

...

{$userSuppliedData|unsafe_noescape} {* DANGER! There could be an XSS problem! *}

In the above code, if $userSuppliedData is already defined and isn't already sanitized, the unsanitized content will be exposed to the user.

To avoid this, Brainy includes {load} and {store}. {store} performs the same behavior as {capture assign="foo"}, except the data is stored in an alternate scope that isn't exposed to anything other than {load}. For example, the above code would be better written as:

{store to="emptyState"}
  This data is <b>captured</b>.
{/store}

...

{if $userSuppliedData}
  {$userSuppliedData|escape} {* Proper escaping! :) *}
{else}
  {load from="emptyState"}
{/if}

Using {load} and {store} is a great way to help encourage developers to avoid potentially risky patterns, and provides a way to avoid needing |unsafe_noescape to begin with.