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 (consolidate batch results).
Install / Use
npx skills add utkusen/sast-skills --skill sast-sstiInstalls into whichever agent you are using.
SKILL.md
Installable skill definition
Quality Score
Category
SecuritySupported Platforms
Our assessment of sast-ssti
sast-ssti scores 89/100 on our quality scale, 494th of 971 Security skills we index.
Its SKILL.md is 29 KB long, well organised into 35 sections with 17 code examples: a thorough specification that gives an agent plenty to work with.
With 1,321 GitHub stars, it is one of the more widely adopted skills in the catalogue.
Maintenance, license and trust
- The repository was last updated about 6 months ago. That is recent enough to be usable, but agent tooling moves fast, so check the instructions against your agent's current version.
- It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
- Its trust signals score 98/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.
sast-ssti compared with similar skills
All 4 of these similar skills score higher than sast-ssti; compare them before choosing.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| sast-ssti (this skill)by utkusen | 89 | 1.3k | 6mo ago | SKILL.md |
| algorithmic-artby anthropics | 100 | 177.9k | 8d ago | SKILL.md |
| pptxby anthropics | 100 | 177.9k | 8d ago | SKILL.md |
| designby nextlevelbuilder | 100 | 130.2k | 9d ago | SKILL.md |
| ui-ux-pro-maxby nextlevelbuilder | 100 | 130.2k | 9d ago | SKILL.md |
Frequently asked questions
- How do I install sast-ssti?
- Run
npx skills add utkusen/sast-skills --skill sast-ssti. The install tabs above show the steps for each supported agent. - Which AI agents does sast-ssti work with?
- It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
- Is sast-ssti safe to use?
- It is MIT-licensed and scores 98/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
- Is sast-ssti still maintained?
- The repository was last updated about 6 months ago. That is recent enough to be usable, but agent tooling moves fast, so check the instructions against your agent's current version.
Skill content
View source on GitHubname: 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()— Jinja2env.from_string(user_input).render()— Jinja2render_template_string(user_input)— Flaskejs.render(user_input, ctx)— EJS (Node.js)nunjucks.renderString(user_input, ctx)— NunjucksHandlebars.compile(user_input)(ctx)— Handlebarspug.render(user_input, ctx)— Pug/Jade_.template(user_input)(ctx)— Lodash/UnderscoreVelocity.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)— Gotext/templateTemplate.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) whereuser_inputis not validated against a safe listres.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 preview():
data = request.form.get('data')
return env.get_template("preview.html").render(data=data)
Node.js — EJS
// VULNERABLE: user input as template string
app.get('/render', (req, res) => {
const tmpl = req.query.template;
res.send(ejs.render(tmpl, { user: req.user }));
// Payload: ?template=<%- global.process.mainModule.require('child_process').execSync('id') %>
});
// SECURE: user input only in context data
app.get('/render', (req, res) => {
res.render('report', { content: req.query.content });
});
Node.js — Nunjucks
// VULNERABLE: renderString with user-controlled template
app.post('/preview', (req, res) => {
const output = nunjucks.renderString(req.body.tmpl, { user: req.user });
res.send(output);
// Payload: {{ range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")() }}
});
// SECURE: render from a file, user input only as context
app.post('/preview', (req, res) => {
res.render('preview.html', { content: req.body.content });
});
Node.js — Handlebars
// VULNERABLE: compile with user-supplied template string
app.get('/email', (req, res) => {
const template = Handlebars.compile(req.query.tmpl);
res.send(template({ user: req.user }));
// Payload: {{#with "s" as |string|}}{{#with "e"}}{{#with split as |conslist|}}...
});
// SECURE: compile static template, user data in context
const template = Handlebars.compile(fs.readFileSync('email.hbs', 'utf8'));
app.get('/email', (req, res) => {
res.send(template({ name: req.query.name }));
});
Ruby — ERB
# VULNERABLE: user input passed to ERB constructor
get '/render' do
tmpl = params[:template]
ERB.new(tmpl).result(binding)
# Payload: <%= `id` %>
end
# SECURE: static ERB file, user data in binding only
get '/render' do
@name = params[:name]
erb :profile
end
Java — FreeMarker
// VULNERABLE: template string sourced from user input
@PostMapping("/preview")
public String preview(@RequestParam String tmplStr, Model model) throws Exception {
Template t = new Template("preview", new StringReader(tmplStr), cfg);
StringWriter out = new StringWriter();
t.process(model.asMap(), out);
return out.toString();
// Payload: <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
}
// SECURE: load template from classpath, user data only in model
@GetMapping("/report")
public String report(@RequestParam String userId, Model model) {
model.addAttribute("user", userService.findById(userId));
return "report"; // resolves to templates/report.ftl
}
Java — Velocity
// VULNERABLE: user input evaluated as template
public String render(String userTemplate) {
VelocityContext ctx = new VelocityContext();
StringWriter sw = new StringWriter();
Velocity.evaluate(ctx, sw, "template", userTemplate);
return sw.toString();
// Payload: #set($e="")#set($x=$e.class.forName("java.lang.Runtime"))...
}
// SECURE: load template from file
Template t = Velocity.getTemplate("report.vm");
t.merge(ctx, sw);
Java — Thymeleaf (Spring)
// VULNERABLE: user input used as template expression evaluated by Thymeleaf
@GetMapping("/hello")
public String hello(@RequestParam String lang, Model model) {
return "user/" + lang + "/welcome"; // path traversal + SSTI if lang is e.g. "__${T(java.lang.Runtime).getRuntime().exec('id')}"
}
// SECURE: validate lang against an allowlist
private static final Set<String> ALLOWED_LANGS = Set.of("en", "fr", "de");
@GetMapping("/hello")
public String hello(@RequestParam String lang, Model model) {
if (!ALLOWED_LANGS.contains(lang)) return "error";
return "user/" + lang + "/welcome";
}
PHP — Twig
// VULNERABLE: user input as template string
$app->get('/render', function (Request $request) use ($twig) {
$tmpl = $request->query->get('template');
return $twig->createTemplate($tmpl)->render([]);
// Payload: {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
});
// SECURE: static template, user data in context array
$app->get('/profile', function (Request $request) use ($twig) {
return $twig->render('profile.html.twig', ['name' => $request->query->get('name')]);
});
PHP — Smarty
// VULNERABLE: user-controlled template string via fetch("string:...")
$template = $_GET['tmpl'];
$smarty->fetch("string:" . $template);
// Payload: {php}echo shell_exec('id');{/php}
// SECURE: pass user data as template variable
$smarty->assign('name', $_GET['name']);
$smarty->display('profile.tpl');
Go — text/template
// VULNERABLE: user input parsed as template
func handler(w http.ResponseWriter, r *http.Request) {
tmpl := r.URL.Query().Get("tmpl")
t, _ := template.New("x").Parse(tmpl)
t.Execute(w, data)
// Payload: {{.Func "os/exec" "id"}} — depends on data methods exposed
}
// SECURE: static template string or file; user input only in data
func handler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("tmpl/page.html"))
t.Execute(w, map[string]string{"Name": r.URL.Query().Get("name")})
}
// Note: Go's html/template auto-escapes output, but text/template does not.
// Even html/template is vulnerable to SSTI if user input reaches .Parse().
Execution
This skill runs in three phases using subagents. Pass the contents of sast/architecture.md to all subagents as context.
Phase 1: Find Template Rendering Sites Using Dynamic Strings
Launch a subagent with the following instructions:
Goal: Find every location in the codebase where a template engine renders, compiles, or evaluates a dynamically built string as the template itself — rather than loading a static template file. Write results to
sast/ssti-recon.md.Context: You will be given the project's architecture summary. Use it to understand the tech s
Truncated for display — read the full file on GitHub.
Related Skills
algorithmic-art
177.9kCreating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems.
pptx
177.9kUse this skill any time a .pptx or .potx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx or .potx file (even if the extracted content will be used elsewhere, like in an em…
design
130.2kComprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini, Atlas Cloud, or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG…
ui-ux-pro-max
130.2kUI/UX design intelligence for web, mobile, and desktop. This skill should be used when designing, building, reviewing, or fixing interfaces, including pages, components, design systems, accessibility, interaction, responsive layout, typography, color, charts, and stack-specific UI implementation.
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
