---
name: detect-ssti
description: "Detection and response methodology for Server-Side Template Injection (SSTI), where user input is evaluated as template code by a template engine, commonly escalating to remote code execution (CWE-1336, CWE-94). Maps to MITRE ATT&CK T1190 and T1059 (.004, .001, .006) once a gadget reaches the shell. Works off web server and reverse-proxy access logs, WAF logs, application error and stack-trace logs carrying template-engine exceptions (Jinja2, Twig, FreeMarker, Thymeleaf, SpEL, Velocity), and host process-creation telemetry (auditd execve, Sysmon Event ID 1). Gives the method for catching template error and payload signatures plus engine-hardening guidance. Use when triaging 500 errors that mention a template engine, hunting for SSTI or template-injection probes, spotting {{7*7}} / ${7*7} / polyglot payloads, investigating a web app server spawning cmd.exe or sh, or hardening a templating stack against injection."
---

# Server-Side Template Injection (SSTI) - Defensive Methodology

> **Tier 2 - methodology skill.** No bespoke UMBRASEC detection rule ships with this skill yet. It gives the defender's method, the log sources and signals, the hunt, and the hardening, and points to public rule sources. Promotion to Tier 1 (a sourced, tuned UMBRASEC rule) is tracked in [COVERAGE.md](https://github.com/umbrasec-labs/umbra-blue/blob/main/COVERAGE.md). Do not treat the content below as citing a UMBRASEC rule; there is none for this threat yet.

## TL;DR for defenders

SSTI happens when an application concatenates user input into a template that is then compiled and evaluated server-side, so the input runs as template code rather than being rendered as data. The reliable payload path leads to remote code execution, which is why this is a high-severity injection class, not a reflected-output bug. You catch it in three places, in rising order of fidelity. First, the **application error log**: a probe that is syntactically invalid for the engine throws a named exception (`jinja2.exceptions.TemplateSyntaxError`, `Twig\Error\SyntaxError`, `freemarker.core.ParseException`, `org.thymeleaf.exceptions.TemplateProcessingException` / `SpelEvaluationException`), usually surfacing as a `500`. Second, the **request logs and WAF**, where the probe leaves template metacharacters (`{{ }}`, `${ }`, `#{ }`, `<% %>`, `%{ }`) and, at the exploitation stage, engine-specific gadget tokens (`__class__`, `__globals__`, `freemarker.template.utility.Execute`, `T(java.lang.Runtime)`). Third, and highest fidelity, **host process telemetry** where the web or app server process spawns a shell or interpreter, which catches successful exploitation regardless of the probe syntax. The durable fix is to never compile untrusted input as a template and to run the engine sandboxed; the detection exists for the cases where that is missing or wrong.

## What it looks like, defender view

A probing attacker is hunting for the engine and the evaluation sink, so the early traffic is small arithmetic and syntax tests sprayed at parameters that end up in rendered output: display names, profile fields, subject lines, error-page echoes, CMS blocks, "preview" features, and PDF or report generators. Classic probes are `{{7*7}}`, `${7*7}`, `#{7*7}`, `<%= 7*7 %>`, `@(7*7)`, and the differentiator `{{7*'7'}}` (Jinja2 returns `7777777`, Twig returns `49`). A single polyglot such as `${{<%[%'"}}%\` is designed to throw an error in several engines at once, so it is a strong probe signature in request logs. Two outcomes follow. When the probe is valid for the engine, the evaluated result appears in the response body (an echoed `49` where the input was `7*7`) and the status is often `200`, which leaves little edge signal beyond the request itself. When the probe is invalid, the engine raises a parse or evaluation exception, the request returns `500`, and the stack trace names the engine in the application log. Exploitation then pivots to a gadget chain that reaches code execution, and the decisive, engine-agnostic tell is a process-creation event where the web or app server process (`python`, `java`, `php-fpm`, `w3wp.exe`, `node`, `ruby`) spawns `sh`, `bash`, `cmd.exe`, or `powershell.exe`.

## Log sources and signals

The signal lives at the edge, in the application, and on the host. The application and host sources are the high-fidelity ones and are often not shipped to the SIEM by default; confirm they are:

- **Application error and stack-trace logs.** The highest-fidelity probe signal. A malformed template payload raises a named, loggable exception keyed to the engine: Jinja2 / Flask `jinja2.exceptions.TemplateSyntaxError` and `UndefinedError`; Twig `Twig\Error\SyntaxError` (legacy `Twig_Error_Syntax`); FreeMarker `freemarker.core.ParseException` and `freemarker.core.InvalidReferenceException`; Thymeleaf / Spring `org.thymeleaf.exceptions.TemplateProcessingException`, `org.springframework.expression.spel.SpelEvaluationException`, with SpEL error codes such as `EL1007E` / `EL1008E`, and OGNL `ognl.OgnlException`; Velocity `org.apache.velocity.exception.ParseErrorException`; Mako `mako.exceptions.SyntaxException`; ERB `(erb):` syntax errors; Smarty `SmartyCompilerException`. Capture the exception class, the message, the request path, and the authenticated principal. These are also emitted into error trackers (Sentry, Rollbar, Datadog APM) if you run one.
- **Web server / reverse-proxy access logs.** nginx (`$request_uri`, `$args`, `$status`, and `$request_body` only if you enable body logging), Apache combined log, IIS W3C fields (`cs-uri-stem`, `cs-uri-query`, `sc-status`). These carry template metacharacters and gadget tokens when the payload rides in the path or query string; payloads in a POST body, JSON, or a header are invisible here unless body logging is on, which is why the application and WAF layers matter.
- **WAF logs.** AWS WAF, Azure WAF / Front Door, Cloudflare, Akamai, and ModSecurity with the OWASP Core Rule Set. These inspect the body and headers the proxy log omits and carry rule-hit metadata. CRS coverage of SSTI is partial, so treat a WAF miss as expected, not as proof of absence.
- **Host process-creation telemetry.** The decisive post-exploitation source. Linux `auditd` `execve` records or `osquery` `process_events`, and Windows Sysmon **Event ID 1** (or Security **4688**). The signal is a parent process that is the web or app server runtime (`python`, `java`, `php-fpm`, `httpd`, `nginx`, `node`, `ruby`, `w3wp.exe`, `dotnet`) spawning a shell or interpreter child (`sh`, `bash`, `cmd.exe`, `powershell.exe`, `whoami`, `id`). This catches exploitation independent of the probe and is the hardest for the attacker to avoid.

What normal looks like: template-engine parse and evaluation exceptions are rare in a healthy app and cluster around deploys, not around one client; rendered parameters contain names and text, not braces and language keywords; and the web or app server runtime does not spawn interactive shells. The anomaly is a departure from each of those baselines, concentrated on one source principal, session, or IP.

## Detection approach

No bespoke UMBRASEC rule ships for this threat yet. Build detection in three layers and lean on public, maintained rule sources rather than inventing a signature.

- **Engine-exception spike (highest precision on the probe).** Alert on template-engine parse and evaluation exception classes (the list above) appearing in application or error-tracker logs, especially when they correlate with `500` responses and a client that sent template metacharacters in the same request. A healthy app throws almost none of these outside deploys, so even a low rate from a single external principal is worth triage. This is the layer least prone to false positives from scanner noise.
- **Request-side payload signatures.** On WAF and proxy logs, match clusters of template metacharacters (`{{`, `${`, `#{`, `<%`, `%{`) and the polyglot `${{<%[%'"}}%\` in parameters that feed rendered output, then escalate on engine gadget tokens that only appear at the exploitation stage: Python / Jinja2 `__class__`, `__mro__`, `__subclasses__`, `__globals__`, `__builtins__`, `__import__`, `lipsum`, `cycler`, `config.__class__`, `request.application`; Java `freemarker.template.utility.Execute`, `T(java.lang.Runtime)`, `Runtime.getRuntime().exec`, `new ProcessBuilder`, `@java.lang.Runtime@getRuntime`; PHP `{php}`, `system(`, `_self.env`; Node `constructor.constructor`, `child_process`, `global.process`. The bare arithmetic probe is noisy; the gadget tokens are high signal because benign traffic almost never contains them.
- **Process-creation correlation (highest fidelity on exploitation).** Alert when a web or app server runtime process spawns a shell or interpreter, as above. This is engine-agnostic and survives payload obfuscation, so make it the backstop that catches what the request-side signatures miss.

Public, maintained rule and reference sources to point detection engineering at:

- SigmaHQ web rules directory: <https://github.com/SigmaHQ/sigma/tree/master/rules/web>, for request-side web-attack and error-based patterns. Retune the IP-keyed rules to your authenticated-principal field where you have one.
- SigmaHQ process-creation rules for a web or application server spawning a shell: the Linux directory <https://github.com/SigmaHQ/sigma/tree/master/rules/linux/process_creation> and the Windows directory <https://github.com/SigmaHQ/sigma/tree/master/rules/windows/process_creation>. These give you the post-exploitation backstop above.
- OWASP Core Rule Set (ModSecurity), for request-body injection signatures you can extend: <https://github.com/coreruleset/coreruleset>.
- MITRE D3FEND Application Configuration Hardening (D3-ACH): <https://d3fend.mitre.org/technique/d3f:ApplicationConfigurationHardening/>.
- OWASP WSTG testing reference for SSTI, for the engine fingerprint and sink definitions: <https://owasp.org/www-project-web-security-testing-guide/>.

## Threat hunt

For the careful attacker who fingerprints once, evaluates one gadget, and stops, no threshold rule fires. Hunt the engine evidence and the process lineage instead:

1. Inventory which parameters and features reach a template render on the server (email and notification templates, display names, error-page echoes, CMS or theme blocks, PDF and report generators, any "preview" of user-supplied content). These are the sinks; the rest of the hunt focuses there.
2. From application and error-tracker logs over a long window, list every template-engine parse or evaluation exception (the class list above). Group by source principal, session, or IP. A legitimate deploy produces a burst from no client; a probe produces them from one external client, often across several of the sink parameters from step 1.
3. On WAF and proxy logs, search the sink parameters for template metacharacter clusters and the polyglot string, then for the engine gadget tokens. Rank hits by whether they reached a gadget token, not by raw volume, so a single exploitation attempt outranks a noisy arithmetic scan.
4. On host telemetry, list every process-creation event where a web or app server runtime is the parent and a shell or interpreter is the child, over the same window. Any such event on an app server that does not legitimately shell out is a high-priority lead; join its timestamp back to the request logs to find the triggering request.
5. Correlate: a `500` with an engine exception, from the same principal and within seconds of a shell spawn on that host, is a near-confirmed exploitation chain. Rank candidate sinks by the sensitivity of the host and its outbound reach.

Vulnerability scanners, DAST tools, bug-bounty researchers, and internal QA will generate probe traffic and engine exceptions legitimately and will top steps 2 and 3. Build the allowlist from that first pass, then alert on what remains. These surface candidates for triage, not verdicts.

## Hardening

The durable fix is in the application, not the detector. SSTI exists because untrusted input reaches the template compile step as template source rather than as data. Pass user input only as bound template variables, never concatenate it into the template string, and never let a user control the template name or body. Where an engine offers a sandbox or a logic-less mode, enable it and keep attribute and method access off by default: Jinja2 `SandboxedEnvironment`, Twig sandbox with a tight policy, FreeMarker `TemplateClassResolver.SAFER_RESOLVER` and a restricted `new` built-in, Thymeleaf restricted expression evaluation. Keep the template engine and web framework patched, because sandbox-escape gadgets are found and fixed regularly. Log engine exceptions and process creation so the detection above is possible. The core "do not compile user input as a template" control is a secure-coding practice with no single clean framework code, so its cells are left blank; the codes below follow the control vocabulary used in the UMBRASEC [foundation guide](https://umbrasec.dev/guide/foundation.html) and [detection and response guide](https://umbrasec.dev/guide/detection-and-response.html), and a cell is left blank where no single control maps cleanly.

| Control | What it does | Essential Eight | ISO 27001:2022 | NIST CSF 2.0 |
|---|---|---|---|---|
| Never compile untrusted input as a template | Removes SSTI at the source: input is bound data, not template source, and users never control the template name or body | - | - | - |
| Enable the engine sandbox / logic-less mode | Limits attribute and method access so a gadget cannot reach `Runtime`, `os`, or `__globals__` even if injection occurs | User application hardening | - | PR.PS Platform security |
| Patch the template engine and framework | Closes known sandbox-escape and parser gadgets as they are disclosed | Patch applications | A.8.8 Technical vulnerabilities | PR.PS Platform security |
| Log engine exceptions and process creation | Supplies the telemetry every detection layer above depends on | ML2/ML3 event-log collection | A.8.15 Logging; A.8.16 Monitoring | DE.CM Continuous monitoring; DE.AE Adverse events |

## Honest limitations

- **A valid probe can be near-silent.** When the injected expression is syntactically valid for the engine, it evaluates to a `200` with the result in the body and throws no exception, so the engine-exception layer sees nothing. You are left with the request-side signature and the response content, which needs body or response inspection you may not have.
- **No body logging, no request-side signal.** If the payload rides in a POST body, JSON, or a header and you log only the URL at the proxy, the metacharacter and gadget signatures never appear; the WAF and application layers must carry that load.
- **WAF coverage of SSTI is partial.** The OWASP CRS and vendor rules catch some template payloads, especially PHP and Java gadgets, but not all engines and not obfuscated or chunked payloads. A clean WAF log is not evidence of no attack.
- **The arithmetic probe is noisy; braces are everywhere.** `{{ }}` and `${ }` appear in legitimate client-side templating, in JSON, and in placeholder text, so the request-side layer alone produces false positives and must be scoped to true server-side render sinks and escalated on gadget tokens.
- **A gadget that avoids the shell dodges the process backstop.** In-process gadgets that read files, open sockets, or call native methods without spawning a child process will not light up the process-creation layer; that layer catches shell and interpreter execution, which is the common but not the only outcome.
- **Legitimate probe traffic exists.** Scanners, DAST, QA, and researchers generate the same exceptions and payloads and need an allowlist tuned to your environment.

## ATT&CK and D3FEND

- MITRE ATT&CK: [T1190 Exploit Public-Facing Application](https://attack.mitre.org/techniques/T1190/) (Initial Access) is the closest honest map, because ATT&CK has no dedicated SSTI or injection technique. Once a gadget reaches code execution, the follow-on is [T1059 Command and Scripting Interpreter](https://attack.mitre.org/techniques/T1059/) (Execution), typically [T1059.004 Unix Shell](https://attack.mitre.org/techniques/T1059/004/), [T1059.001 PowerShell](https://attack.mitre.org/techniques/T1059/001/), or [T1059.006 Python](https://attack.mitre.org/techniques/T1059/006/), which is what the process-creation detection layer keys off.
- MITRE D3FEND: [D3-ACH Application Configuration Hardening](https://d3fend.mitre.org/technique/d3f:ApplicationConfigurationHardening/) - harden the template engine configuration (enable the sandbox, disable dangerous built-ins and method access) to reduce the attack surface a template injection can reach.
- Weakness and reference sources: CWE-1336 (Improper Neutralization of Special Elements Used in a Template Engine), CWE-94 (Code Injection), [OWASP A03:2021 Injection](https://owasp.org/Top10/A03_2021-Injection/), [OWASP Web Security Testing Guide](https://owasp.org/www-project-web-security-testing-guide/), [SigmaHQ rules](https://github.com/SigmaHQ/sigma), and the [OWASP Core Rule Set](https://github.com/coreruleset/coreruleset).

**Scope note.** This is a defensive skill. It explains SSTI only to the depth needed to detect and mitigate it and deliberately provides no exploitation payloads, gadget chains, or sandbox-escape tooling. UMBRASEC publishes defense, not offense.
