Server-Side Template Injection (SSTI): The Complete Practical Guide

Preventing SSTI

How to stop SSTI: never render user input as a template, use logic-less templates, sandbox as defense in depth, and cover the hidden sinks.

Published

Every SSTI comes from one mistake: user input reaching the template string instead of the template's data. So the fix is singular and reliable. Keep user input out of the template source, and there is nothing for the engine to evaluate. Everything else is defense in depth behind that. This page covers the controls in priority order. Build the habit on a free live target.

The one fix that matters: never render user input as a template

Pass user input to the engine as a context variable, so the engine treats it as data to display, never as template code to run:

# VULNERABLE: input concatenated into the template source
Template("Hello " + name).render()
 
# SAFE: input passed as data; "Hello {{ name }}" is a fixed, trusted template
Template("Hello {{ name }}").render(name=name)

The template itself must be a fixed, developer-authored string. User input only ever fills a placeholder. Done consistently, this removes SSTI entirely, because a probe like the 7-times-7 expression lands in a value slot and is displayed verbatim, never evaluated.

Prefer logic-less templates for untrusted content

When a feature genuinely needs user-influenced templates (user-customizable emails, themes, report layouts), use a logic-less engine such as Mustache. These only substitute values and iterate simple sections, with no object model to walk, so even mishandled input cannot reach an execution primitive. Trading template power for a tiny attack surface is the right call anywhere untrusted input touches rendering.

Enable the sandbox, but do not trust it

If you must use a capable engine, turn on its sandbox and configure it strictly:

  • Jinja2: use SandboxedEnvironment, keep os and other dangerous objects out of the context, and avoid exposing globals that carry __globals__.
  • Twig: enable the sandbox with a minimal policy, and crucially do not allow callback-taking filters (filter, map, sort, reduce) on untrusted templates.
  • Freemarker: restrict the class resolver (TemplateClassResolver safe settings) and remove the Execute/ObjectConstructor built-ins.

Treat all of this as a backstop. Sandboxes are escapable, as the sandbox-escape spoke shows, so they buy time, not safety.

Escape the XSS that rides along

Even after SSTI is fixed, user input rendered into a page still needs context-aware output escaping, or you have traded SSTI for cross-site scripting. Most engines auto-escape HTML by default. Keep that on and do not mark untrusted input as safe.

Cover the non-obvious sinks

SSTI hides wherever a server builds output from a template: emails, generated PDFs and invoices, error and notification pages, dynamic themes, and even filenames that flow into a templated export. Audit all of these as template sinks, not just web pages, because they are the usual home for blind SSTI.

Patch and minimize

Keep the template engine updated so known sandbox-escape and RCE bugs are fixed, and remove dangerous built-ins, extensions, and tags you do not use (legacy Smarty {php}, Freemarker's Execute, and similar).

A short checklist

  • Never render user input as a template. Pass it as a context variable.
  • Use a logic-less engine for anything user-customizable.
  • Enable the engine sandbox strictly, as defense in depth only.
  • Keep dangerous objects and callback-taking filters out of untrusted contexts.
  • Context-aware output escaping for the XSS that follows.
  • Audit emails, PDFs, themes, and filenames as template sinks.
  • Patch the engine and remove built-ins you do not need.

That is the whole defense. For the offensive side in one place, see the SSTI cheat sheet, and to prove to yourself that passing input as a variable really does close the bug, break a concatenated-template version on a free live target.

← Back to Server-Side Template Injection (SSTI): The Complete Practical Guide