$npx -y skills add utkusen/sast-skills --skill sast-sstiDetect Server-Side Template Injection (SSTI) vulnerabilities in a codebase using a three-phase approach: recon (find template rendering sites that use dynamic strings), batched verify (trace user input to those sites in parallel subagents, 3 candidates each), and merge (consolida
| 1 | # Server-Side Template Injection (SSTI) Detection |
| 2 | |
| 3 | You are performing a focused security assessment to find Server-Side Template Injection vulnerabilities in a codebase. This skill uses a three-phase approach with subagents: **recon** (find candidate rendering sites where the template string is dynamic), **batched verify** (trace whether user input reaches each site's template argument, in parallel batches of 3), and **merge** (consolidate batch results into the final report). |
| 4 | |
| 5 | **Prerequisites**: `sast/architecture.md` must exist. Run the analysis skill first if it doesn't. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## What is SSTI |
| 10 | |
| 11 | Server-Side Template Injection occurs when user-supplied input is embedded directly into a template string that is then evaluated by a template engine. Unlike passing user data as *context variables* to a static template, SSTI means the user can write template syntax that the engine will execute — leading to arbitrary code execution, file read, or full server compromise. |
| 12 | |
| 13 | The core pattern: *unvalidated user input is used as the template string passed to a template engine's render/compile/evaluate function.* |
| 14 | |
| 15 | ### What SSTI IS |
| 16 | |
| 17 | - Passing user input as the template string to be compiled or rendered: |
| 18 | - `Template(user_input).render()` — Jinja2 |
| 19 | - `env.from_string(user_input).render()` — Jinja2 |
| 20 | - `render_template_string(user_input)` — Flask |
| 21 | - `ejs.render(user_input, ctx)` — EJS (Node.js) |
| 22 | - `nunjucks.renderString(user_input, ctx)` — Nunjucks |
| 23 | - `Handlebars.compile(user_input)(ctx)` — Handlebars |
| 24 | - `pug.render(user_input, ctx)` — Pug/Jade |
| 25 | - `_.template(user_input)(ctx)` — Lodash/Underscore |
| 26 | - `Velocity.evaluate(ctx, user_input)` — Apache Velocity (Java) |
| 27 | - `new Template("anon", new StringReader(user_input), cfg).process(...)` — FreeMarker (Java) |
| 28 | - `new ST(user_input).render()` — StringTemplate4 (Java) |
| 29 | - `thymeleafEngine.process(user_input, ctx)` — Thymeleaf (Java) |
| 30 | - `\Twig\Environment::createTemplate(user_input)->render(ctx)` — Twig (PHP) |
| 31 | - `$smarty->fetch("string:" . user_input)` — Smarty (PHP) |
| 32 | - `Liquid::Template.parse(user_input).render(ctx)` — Liquid (Ruby) |
| 33 | - `ERB.new(user_input).result(binding)` — ERB (Ruby) |
| 34 | - `t, _ := template.New("x").Parse(user_input); t.Execute(w, data)` — Go `text/template` |
| 35 | - `Template.fromString(user_input).render(ctx)` — Pebble (Java) |
| 36 | |
| 37 | - Dynamic template name construction where the name itself comes from user input and the engine resolves arbitrary files: |
| 38 | - `render_template(user_input)` (Flask) where `user_input` is not validated against a safe list |
| 39 | - `res.render(req.query.template)` (Express) where the template name is user-controlled |
| 40 | |
| 41 | ### What SSTI is NOT |
| 42 | |
| 43 | Do not flag these patterns: |
| 44 | |
| 45 | - **User input as context data** (safe — the template is static, only the data changes): |
| 46 | ``` |
| 47 | render_template("profile.html", name=request.args.get("name")) |
| 48 | env.get_template("report.html").render(user=user_obj) |
| 49 | res.render("dashboard", { title: req.body.title }) |
| 50 | ``` |
| 51 | - **XSS via template output**: If the template outputs unsanitized user data that is then rendered in a browser — that's XSS, not SSTI |
| 52 | - **Static templates with dynamic filenames validated against an allowlist**: If the template name comes from user input but is strictly validated against a hardcoded set of allowed template names, it's not SSTI |
| 53 | - **Sandboxed template engines configured with a restricted environment**: Liquid, Mustache, and similar logic-less engines cannot execute arbitrary code even if the template string comes from user input — but still flag them as "Needs Manual Review" unless you can confirm the engine is logic-less |
| 54 | |
| 55 | ### Patterns That Prevent SSTI |
| 56 | |
| 57 | When you see these patterns, the code is likely **not vulnerable**: |
| 58 | |
| 59 | **1. Static template file with dynamic context (most common safe pattern)** |
| 60 | ```python |
| 61 | # Flask — static template, user input only in context dict |
| 62 | return render_template("user_profile.html", username=request.args.get("name")) |
| 63 | |
| 64 | # Express — static view name |
| 65 | res.render("dashboard", { user: req.user }) |
| 66 | ``` |
| 67 | |
| 68 | **2. Allowlist validation for template names** |
| 69 | ```python |
| 70 | ALLOWED_TEMPLATES = {"invoice.html", "receipt.html", "summary.html"} |
| 71 | template_name = request.args.get("tmpl", "invoice.html") |
| 72 | if template_name not in ALLOWED_TEMPLATES: |
| 73 | abort(400) |
| 74 | return render_template(template_name) |
| 75 | ``` |
| 76 | |
| 77 | **3. Logic-less / sandboxed engines that don't support code execution** |
| 78 | ```javascript |
| 79 | // Mustache — logic-less, cannot execute arbitrary code even if template is user-supplied |
| 80 | const output = Mustache.r |