/sast-ssti
Detect 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
$ npx -y skills add utkusen/sast-skills --skill sast-ssti --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/sast-ssti
Context preview
The summary Claude sees to decide when to auto-load this skill.
Detect 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
SKILL.md
sast-ssti.SKILL.mdname: sast-ssti
description: >-
Detect 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 (consolidate batch results). Requires
sast/architecture.md (run sast-analysis first). Outputs findings to
sast/ssti-results.md. Use when asked to find SSTI or template injection bugs.
Server-Side Template Injection (SSTI) Detection
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).
**Prerequisites**: `sast/architecture.md` must exist. Run the analysis skill first if it doesn't.
---
What is SSTI
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.
The core pattern: *unvalidated user input is used as the template string passed to a template engine's render/compile/evaluate function.*
What SSTI IS
- Passing user input as the template string to be compiled or rendered:
- `Template(user_input).render()` — Jinja2
- `env.from_string(user_input).render()` — Jinja2
- `render_template_string(user_input)` — Flask
- `ejs.render(user_input, ctx)` — EJS (Node.js)
- `nunjucks.renderString(user_input, ctx)` — Nunjucks
- `Handlebars.compile(user_input)(ctx)` — Handlebars
- `pug.render(user_input, ctx)` — Pug/Jade
- `_.template(user_input)(ctx)` — Lodash/Underscore
- `Velocity.evaluate(ctx, user_input)` — Apache Velocity (Java)
- `new Template("anon", new StringReader(user_input), cfg).process(...)` — FreeMarker (Java)
- `new ST(user_input).render()` — StringTemplate4 (Java)
- `thymeleafEngine.process(user_input, ctx)` — Thymeleaf (Java)
- `\Twig\Environment::createTemplate(user_input)->render(ctx)` — Twig (PHP)
- `$smarty->fetch("string:" . user_input)` — Smarty (PHP)
- `Liquid::Template.parse(user_input).render(ctx)` — Liquid (Ruby)
- `ERB.new(user_input).result(binding)` — ERB (Ruby)
- `t, _ := template.New("x").Parse(user_input); t.Execute(w, data)` — Go `text/template`
- `Template.fromString(user_input).render(ctx)` — Pebble (Java)
- Dynamic template name construction where the name itself comes from user input and the engine resolves arbitrary files:
- `render_template(user_input)` (Flask) where `user_input` is not validated against a safe list
- `res.render(req.query.template)` (Express) where the template name is user-controlled
What SSTI is NOT
Do not flag these patterns:
- **User input as context data** (safe — the template is static, only the data changes):
render_template("profile.html", name=request.args.get("name"))
env.get_template("report.html").render(user=user_obj)
res.render("dashboard", { title: req.body.title })- **XSS via template output**: If the template outputs unsanitized user data that is then rendered in a browser — that's XSS, not SSTI
- **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
- **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
Patterns That Prevent SSTI
When you see these patterns, the code is likely **not vulnerable**:
**1. Static template file with dynamic context (most common safe pattern)**
# Flask — static template, user input only in context dict
return render_template("user_profile.html", username=request.args.get("name"))
# Express — static view name
res.render("dashboard", { user: req.user })**2. Allowlist validation for template names**
ALLOWED_TEMPLATES = {"invoice.html", "receipt.html", "summary.html"}
template_name = request.args.get("tmpl", "invoice.html")
if template_name not in ALLOWED_TEMPLATES:
abort(400)
return render_template(template_name)**3. Logic-less / sandboxed engines that don't support code execution**
// Mustache — logic-less, cannot execute arbitrary code even if template is user-supplied
const output = Mustache.render(userTemplate, ctx); // lower risk, but still flag for review
---
Vulnerable vs. Secure Examples
Python — Flask / Jinja2
# VULNERABLE: user input rendered as template string
@app.route('/greet')
def greet():
name = request.args.get('name', '')
template = f"<h1>Hello {name}!</h1>"
return render_template_string(template)
# Payload: ?name={{7*7}} → renders "49"
# RCE: ?name={{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# SECURE: user input passed as context variable to a static template
@app.route('/greet')
def greet():
name = request.args.get('name', '')
return render_template("greet.html", name=name)# VULNERABLE: env.from_string with user-controlled template
@app.route('/preview')
def preview():
tmpl = request.form.get('template')
return Environment().from_string(tmpl).render()
# SECURE: load template from trusted file, pass user data as context
@app.route('/preview')
def previewRead more
name: sast-ssti description: >- Detect 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 (consolidate batch results). Requires sast/architecture.md (run sast-analysis first). Outputs findings to sast/ssti-results.md. Use when asked to find SSTI or template injection bugs.
Server-Side Template Injection (SSTI) Detection
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).
**Prerequisites**: `sast/architecture.md` must exist. Run the analysis skill first if it doesn't.
---
What is SSTI
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.
The core pattern: *unvalidated user input is used as the template string passed to a template engine's render/compile/evaluate function.*
What SSTI IS
- Passing user input as the template string to be compiled or rendered:
- `Template(user_input).render()` — Jinja2
- `env.from_string(user_input).render()` — Jinja2
- `render_template_string(user_input)` — Flask
- `ejs.render(user_input, ctx)` — EJS (Node.js)
- `nunjucks.renderString(user_input, ctx)` — Nunjucks
- `Handlebars.compile(user_input)(ctx)` — Handlebars
- `pug.render(user_input, ctx)` — Pug/Jade
- `_.template(user_input)(ctx)` — Lodash/Underscore
- `Velocity.evaluate(ctx, user_input)` — Apache Velocity (Java)
- `new Template("anon", new StringReader(user_input), cfg).process(...)` — FreeMarker (Java)
- `new ST(user_input).render()` — StringTemplate4 (Java)
- `thymeleafEngine.process(user_input, ctx)` — Thymeleaf (Java)
- `\Twig\Environment::createTemplate(user_input)->render(ctx)` — Twig (PHP)
- `$smarty->fetch("string:" . user_input)` — Smarty (PHP)
- `Liquid::Template.parse(user_input).render(ctx)` — Liquid (Ruby)
- `ERB.new(user_input).result(binding)` — ERB (Ruby)
- `t, _ := template.New("x").Parse(user_input); t.Execute(w, data)` — Go `text/template`
- `Template.fromString(user_input).render(ctx)` — Pebble (Java)
- Dynamic template name construction where the name itself comes from user input and the engine resolves arbitrary files:
- `render_template(user_input)` (Flask) where `user_input` is not validated against a safe list
- `res.render(req.query.template)` (Express) where the template name is user-controlled
What SSTI is NOT
Do not flag these patterns:
- **User input as context data** (safe — the template is static, only the data changes):
render_template("profile.html", name=request.args.get("name"))
env.get_template("report.html").render(user=user_obj)
res.render("dashboard", { title: req.body.title })- **XSS via template output**: If the template outputs unsanitized user data that is then rendered in a browser — that's XSS, not SSTI
- **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
- **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
Patterns That Prevent SSTI
When you see these patterns, the code is likely **not vulnerable**:
**1. Static template file with dynamic context (most common safe pattern)**
# Flask — static template, user input only in context dict
return render_template("user_profile.html", username=request.args.get("name"))
# Express — static view name
res.render("dashboard", { user: req.user })**2. Allowlist validation for template names**
ALLOWED_TEMPLATES = {"invoice.html", "receipt.html", "summary.html"}
template_name = request.args.get("tmpl", "invoice.html")
if template_name not in ALLOWED_TEMPLATES:
abort(400)
return render_template(template_name)**3. Logic-less / sandboxed engines that don't support code execution**
// Mustache — logic-less, cannot execute arbitrary code even if template is user-supplied const output = Mustache.render(userTemplate, ctx); // lower risk, but still flag for review
---
Vulnerable vs. Secure Examples
Python — Flask / Jinja2
# VULNERABLE: user input rendered as template string
@app.route('/greet')
def greet():
name = request.args.get('name', '')
template = f"<h1>Hello {name}!</h1>"
return render_template_string(template)
# Payload: ?name={{7*7}} → renders "49"
# RCE: ?name={{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# SECURE: user input passed as context variable to a static template
@app.route('/greet')
def greet():
name = request.args.get('name', '')
return render_template("greet.html", name=name)# VULNERABLE: env.from_string with user-controlled template
@app.route('/preview')
def preview():
tmpl = request.form.get('template')
return Environment().from_string(tmpl).render()
# SECURE: load template from trusted file, pass user data as context
@app.route('/preview')
def previewA collection of agent skills that turn your LLM coding assistant into a fully functional SAST scanner to find vulnerabilities in your codebase. Works natively with Claude Code, Codex, Opencode, Cursor and any other assistant that supports agent skills.
Repo: utkusen/sast-skills
Other skills on sast-skills.
- /sast-analysis
Perform codebase analysis and architecture mapping as the first phase of a security assessment. Explores the tech stack, frameworks, entry points, data flows, and trust boundaries. Outputs sast/architecture.md. Run this before any vulnerability detection skill. Use when asked to
Open skill - /sast-businesslogic
Detect business logic vulnerabilities in a codebase using a three-phase approach: threat modeling (domain analysis and attack scenarios), batched verify (check exploitable gaps in parallel subagents, 3 scenarios each), and merge (consolidate batch results). Covers price
Open skill - /sast-fileupload
Detect insecure file upload vulnerabilities in a codebase using a three-phase approach: discovery (find all upload sites), batched verify (check extension bypass and related issues in parallel subagents, 3 sites each), and merge (consolidate batch results). Requires
Open skill - /sast-graphql
Detect GraphQL injection vulnerabilities in a codebase using a three-phase approach: recon (confirm GraphQL usage and find unsafe operation document assembly sites), batched verify (trace user input to those sites in parallel subagents, up to 3 candidate sites each), and merge
Open skill - /sast-hardcodedsecrets
Detect hardcoded sensitive data (API keys, access tokens, private keys, passwords, etc.) in publicly accessible code — frontend JavaScript, mobile apps, client-side bundles, and HTML templates. Uses a three-phase approach: recon (find secret candidates), batched verify (confirm
Open skill - /sast-idor
Detect Insecure Direct Object Reference (IDOR) vulnerabilities in a codebase using a three-phase approach: recon (find candidates), batched verify (check authorization in parallel subagents, 3 candidates each), and merge (consolidate batch results). Checks endpoints for missing
Open skill

