Lion Cipher Lab
Exploit and fix Server-Side Template Injection (SSTI)
- AppSec / DevSecOps
- Intermediate
- ~50 min
- Free · no account required
Why it matters
Server-Side Template Injection is one of the highest-impact web bugs because it frequently escalates all the way to remote code execution on the server. A template engine (Jinja2 in Flask, Twig in PHP, Freemarker/Velocity in Java, ERB in Ruby) is meant to combine a developer-written TEMPLATE with runtime DATA — 'Hello {{ name }}' plus name='Ada' gives 'Hello Ada'. The vulnerability appears the moment user input is placed into the TEMPLATE half instead of the DATA half: if the code builds the template by concatenating user text (render_template_string("Hello " + name)), the engine compiles and…
What you'll learn
- explaining Server-Side Template Injection (SSTI, CWE-1336 / CWE-94): an app that concatenates user input INTO a template and renders it makes the template engine evaluate attacker input as template code on the server — unlike XSS, which runs in the victim's browser, SSTI runs on your server and often escalates to remote code execution
- building a tiny vulnerable Flask endpoint on your own machine that does the classic mistake, render_template_string("Hello " + name), so user input becomes part of the template source
- DETECTING SSTI with the canonical polyglot probe {{7*7}} and reading the result: getting 49 back proves the server EVALUATED the expression (Jinja2 arithmetic), not merely reflected the literal text {{7*7}}
- ESCALATING benignly on localhost: leaking a SECRET_KEY you planted in the app config with {{ config.items() }}, then walking the Jinja2 object-traversal chain (''.__class__.__mro__ -> object -> __subclasses__, or the shorter lipsum.__globals__['os'] gadget) to run a harmless whoami
- understanding that the traversal generalizes across engines (Twig, Freemarker, Velocity, ERB each have their own gadget chains) because the root cause is identical: user input reaching the template compiler
- FIXING it the right way: never render user input as a template — pass it as DATA into a FIXED template (render_template_string("Hello {{ name }}", name=name) or render_template with a file) so autoescaping treats the value as text, not code
- PROVING the fix by re-sending the exact same {{7*7}} and watching it render literally as the text {{7*7}} instead of 49
- judgment: why a blocklist of {{ or __class__ loses (alternate {% %} syntax, attribute-vs-index access, other engines, encoding) and why Jinja2's SandboxedEnvironment reduces but does not guarantee safety — the durable fix is architectural, not a filter
Steps in this lab
- What SSTI is — the vocabulary, how it differs from XSS, and the ground rules
- Set up: confirm Python, make a folder, install Flask
- Build the VULNERABLE app (it concatenates user input into the template)
- DETECT: send {{7*7}} and read the 49
- ESCALATE (benign): leak the planted secret, then reach the OS
- Why blocklisting {{ (and trusting the sandbox) is the wrong fix
- FIX: pass user input as DATA into a fixed template, then prove it
- What you did — and the reflex to keep