server-side-template-injection
Skill2 starsChanged 4 months agoMITView on GitHub
SAST detection methodology for server-side template injection (CWE-1336, CWE-94), including Jinja2/Twig/Freemarker/Velocity/ERB SSTI→RCE, Spring SpEL/OGNL, sandbox escapes, and client-side template injection. Use when reviewing code that renders templates, especially where user input becomes part of the template itself. Writes confirmed findings to findings/05-server-side-template-injection.md.
- Reads credentials
In this repository49 other files it gives its agents.
- authentication-flawsSkill
- broken-access-control-idorSkill
- business-logic-flawsSkill
- cicd-and-pipeline-injectionSkill
- code-injectionSkill
- command-injectionSkill
- cors-misconfigurationSkill
- crlf-and-header-injectionSkill
- cross-site-scriptingSkill
- csrfSkill
- dependency-and-supply-chainSkill
- dom-clobbering-and-postmessageSkill
- graphql-injection-and-abuseSkill
- hardcoded-secretsSkill
- http-request-smugglingSkill
- iac-misconfigurationSkill
- information-disclosureSkill
- insecure-deserializationSkill
- insecure-randomnessSkill
- jwt-vulnerabilitiesSkill
- ldap-injectionSkill
- llm-and-ai-application-securitySkill
- mass-assignmentSkill
- memory-safetySkill
- mobile-android-securitySkill
- mobile-ios-securitySkill
- nosql-injectionSkill
- oauth-oidc-saml-flawsSkill
- open-redirectSkill
- path-traversal-and-file-inclusionSkill
- prototype-pollutionSkill
- race-conditions-and-toctouSkill
- redosSkill
- sast-final-reportSkill
- sast-finding-validationSkill
- sast-methodologySkill
- security-logging-and-monitoring-failuresSkill
- security-misconfiguration-and-headersSkill
- session-managementSkill
- smart-contract-securitySkill
- sql-injectionSkill
- ssrfSkill
- unrestricted-file-uploadSkill
- unsafe-parsers-and-yamlSkill
- weak-cryptographySkill
- web-cache-poisoning-and-deceptionSkill
- websocket-securitySkill
- xpath-and-xml-injectionSkill
- xxeSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 4 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 45
All HTTP inputs, but especially fields intended as *content*: display names, subjects, bios, custom email/report templates, filenames, and …
- Distinctive: line 92
If users must supply templates, use a logic-less engine (Mustache) or a genuinely sandboxed one with a strict allowlist of …
- Very specific: line 95
Keep autoescaping ON for the correct context (HTML/attr/JS/URL) to prevent the XSS variant.
- Distinctive: line 123
Assess sandboxing: if a sandbox is used, note the engine/version and treat escapes as possible.
- Very specific: line 159
Sandbox escapes: treat Jinja2 SandboxedEnvironment, Twig sandbox, Freemarker restrictions as *hardening*, not a boundary — each has public …
- Distinctive: line 186
Never render user input as a template — pass it as escaped data into a static template.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Server-Side Template Injection — SAST Methodology
Class: Server-Side Template Injection · CWE-1336 (and CWE-94) ·
OWASP: A03 Injection
Findings file: findings/05-server-side-template-injection.md
1. Overview
SSTI occurs when attacker-controlled data is embedded into a template that is then
compiled and evaluated by the template engine — so the input is interpreted as
template syntax, not just data. Because template languages expose object graphs,
built-ins, and (in many engines) arbitrary method calls, SSTI typically escalates
to remote code execution. The decisive distinction: rendering user input AS a
template (render_template_string(user)) is SSTI; interpolating user input
INTO a template's data context (render("page.html", name=user)) is normal and
safe unless output-escaping is off (that is XSS, a different class).
2. Where it lives
- Templates built by concatenation, then rendered:
Template(header + user),render_template_string(f"...{user}..."). - Email/notification/report systems that let users author templates or subjects ("Hi {{name}}") and render them server-side.
- CMS/marketing tools with "liquid"/"twig"/"mustache" fields editable by users.
- Framework surfaces that evaluate expressions: Spring SpEL in
@Value/Thymeleaf/@PreAuthorize, Struts2 OGNL, Freemarker/Velocity in Java apps, ERB in Rails, Handlebars/EJS/Pug in Node, Mako/Jinja2 in Python, Smarty/ Twig/Blade in PHP. - Client-side frameworks rendering server-provided strings as templates (Angular, Vue, AngularJS) → CSTI.
3. Sources (tainted input)
All HTTP inputs, but especially fields intended as content: display names, subjects, bios, custom email/report templates, filenames, and any "personalize this message" feature. Second-order is common: a template snippet stored at one step (profile, campaign) and rendered later for other users. Also config/tenant-authored templates and webhook/third-party content rendered server-side.
4. Sinks (dangerous operations)
# Python (Jinja2 / Mako) — dangerous
from flask import render_template_string
render_template_string("Hello " + name) # input IS the template → RCE
Template("Hi {}".format(user)).render() # Jinja2 Template() on input
mako.template.Template(user).render() # Mako executes Python
# Payload probe: {{7*7}} → 49 ; {{''.__class__.__mro__[1].__subclasses__()}} → RCE// Java — dangerous
new Template("t", new StringReader("Hi " + user), cfg).process(m, out); // Freemarker
Velocity.evaluate(ctx, w, "log", "Hi " + user); // Velocity
parser.parseExpression(user).getValue(); // Spring SpEL → T(Runtime)
// Thymeleaf: fragment/expression from input → SpEL RCE# Ruby (ERB) — dangerous
ERB.new("Hello #{params[:name]}").result(binding) # ERB compiles Ruby
Liquid::Template.parse(user).render # Liquid (safer, but misuse leaks)// Node — dangerous
const t = handlebars.compile(req.body.tpl); // user-authored template
ejs.render(req.query.tpl, data); # EJS runs JS via <%= %>
pug.render(userTemplate); // Pug compiles to JS// PHP (Twig / Smarty) — dangerous
$twig->createTemplate($_GET['tpl'])->render(); // Twig on input (sandbox escapable)
$smarty->display("string:".$_GET['tpl']); // Smarty string resource → PHP5. Sanitizers / safe patterns
Safe:
- Never render user input as a template. Pass it as a variable into a static,
pre-authored template file (
render_template("page.html", name=user)), letting the engine escape it as data. - If users must supply templates, use a logic-less engine (Mustache) or a genuinely sandboxed one with a strict allowlist of filters/attributes and no object/attribute access, method calls, or globals.
- Keep autoescaping ON for the correct context (HTML/attr/JS/URL) to prevent the XSS variant.
Fails / not a real sanitizer:
- Blocklisting
{{,}},__,class— engines offer alternative syntax ({% %},${},#{}, attribute access viaattr(), hex/unicode) and gadget chains route around any keyword list. - Relying on the engine's sandbox (Jinja2
SandboxedEnvironment, Twig sandbox, Freemarker?api, VelocitySecureUberspector) — each has documented escapes; a sandbox reduces but does not eliminate risk, and misconfiguration re-opens it. - HTML-escaping the input — SSTI executes before output escaping; escaping stops XSS, not template evaluation.
- Sanitizing the value but still building the template string via concatenation.
6. Detection methodology
- Find sinks: grep for engines rendering strings (not template files).
rg -n 'render_template_string|Template\(|from_string|env\.from_string' rg -n 'new Template\(|Velocity\.evaluate|freemarker|parseExpression|SpelExpression|Ognl\.' rg -n 'ERB\.new|Liquid::Template\.parse|Mustache\.render' rg -n 'handlebars\.compile|ejs\.render|pug\.(render|compile)|_\.template\(' rg -n 'createTemplate|Smarty|->display\([\'"]string:|Twig' - Decide template-vs-data: is user input part of the template string (SSTI) or only a bound variable (safe/XSS-only)? This is the whole call.
- Trace to a source (direct or second-order stored template). Stop at a constant/template file on disk.
- Assess sandboxing: if a sandbox is used, note the engine/version and treat escapes as possible; if none, it is likely RCE.
- Confirm reachability and pre-auth status. Note framework-expression surfaces (SpEL/OGNL/Thymeleaf) that don't look like "templates" but evaluate expressions — cross-reference the code-injection skill.
- Classify engine capability: Python/Ruby/PHP/Freemarker/Velocity/SpEL/OGNL → RCE-capable; Mustache/Liquid → usually data-leak/limited unless misused.
7. Modern & niche variants
- Jinja2 → RCE: probe
{{7*7}}; escalate via the object graph —{{''.__class__.__mro__[1].__subclasses__()}}to findsubprocess/os, or{{cycler.__init__.__globals__.os.popen('id').read()}},{{request.application.__globals__.__builtins__.__import__('os')}}.config,lipsum,cycler,joinerare common gadget entrypoints even under partial sandboxes. - Freemarker / Velocity: Freemarker
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}; Velocity$class.inspect("java.lang.Runtime")...— both reachRuntimefor RCE. - Thymeleaf: expression/fragment injection evaluates SpEL —
${T(java.lang.Runtime).getRuntime().exec(...)}; also fragment-selector injection (__${...}__::). - Twig / Smarty (PHP): Twig
{{_self.env.registerUndefinedFilterCallback ("system")}}/{{_self.env.getFilter("id")}}; Smarty{php}/{system(...)}andstring:resources → PHP execution. Twig sandbox is escapable via_self/filtergadgets. - ERB / Slim / Haml (Ruby): ERB
<%= system('id') %>— full Ruby, trivially RCE when input is the template. - Handlebars / EJS / Pug / Nunjucks (Node): Handlebars via prototype/helper
gadgets and
Functionconstructor; EJS/Pug compile to JS and exposeprocess/require. Nunjucks ({{range.constructor("...")()}}). - Mako: executes arbitrary Python inline (
<% %>,${}) with no sandbox — any input-as-template is immediate RCE. - Spring SpEL / OGNL injection: the non-obvious SSTI cousins. SpEL in
@Value("#{...}"),@PreAuthorize, SpEL-backed queries; OGNL in Struts2 (%{...}, historic CVEs).T(...)type references reach any class → RCE. - Sandbox escapes: treat Jinja2
SandboxedEnvironment, Twig sandbox, Freemarker restrictions as hardening, not a boundary — each has public escapes; flag input-as-template even when a sandbox is present. - Client-side template injection (CSTI): AngularJS
{{}}/ Angular / Vue rendering server-supplied strings as templates → sandbox-escape XSS (constructor.constructor), sometimes escalating beyond the CSP. Distinct from server RCE but shares the template-vs-data root cause.
8. Common false positives
- User input passed only as a bound variable into a static template file
(
render_template("t.html", x=user)) — that is data interpolation, not SSTI. - Logic-less Mustache with no lambdas and no attribute access.
- Template strings assembled solely from constants/allowlisted fragments.
- Autoescaped output where the concern is XSS, not template evaluation (route to the XSS skill instead).
9. Severity & exploitability
Base Critical for RCE-capable engines (Jinja2, Mako, ERB, Freemarker,
Velocity, Smarty, Twig-with-escape, SpEL/OGNL) — full RCE. High when a
genuine sandbox meaningfully constrains impact to data disclosure, or for CSTI
(client-side execution / XSS). Raise for pre-auth/default-config reachability. See
references/severity-model.md.
10. Remediation
Never render user input as a template — pass it as escaped data into a static template. If user-authored templates are a hard requirement, use a logic-less engine or a strictly sandboxed one with attribute access, method calls, and globals disabled and a filter allowlist. Lock down SpEL/OGNL evaluation contexts and avoid expression evaluation on untrusted input. Keep contextual autoescaping on.
11. Output
Append each confirmed finding to findings/05-server-side-template-injection.md
using references/finding-template.md. Set Class: Server-Side Template
Injection, CWE: CWE-1336 (or CWE-94 for expression-engine cases), and in
Chain potential name primitives such as remote code execution (terminal),
arbitrary file read (template include/globals), or info disclosure (config/
secrets via the object graph under a sandbox — leaked creds feed other chains).
For CSTI, note client-side code execution / XSS.
Primitives (controlled): provides CODE_EXEC; consumes none
References
- CWE-1336; CWE-94; OWASP A03:2021 Injection; PortSwigger SSTI research; OWASP Testing Guide — Template Injection.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

