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.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.