Template injection
Prevent server-side template injection — user input reaching a template compiler, double-render pipelines, sandbox escapes, and template-driven resource exhaustionFrom its SKILL.md
npx -y skills add ShieldNet-360/secure-vibe --skill template-injectionAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 15 stars15 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
6.9 KB, ~1.4k tokens by cl100k_base, as published. Nobody here has run it
Template Injection (SSTI)
Rules (for AI agents)
ALWAYS
- Pass user data to a template only as bound values / context variables, never
concatenated or interpolated into the template source string.
render(tmpl, {name})is safe;render("Hello " + name)is SSTI. - Keep the set of template sources static and trusted — load them from files or constants the user cannot influence. A template whose text comes from the DB, an API response, or a request field is attacker-controlled code, not data.
- Treat any second render pass as code execution. If the output of one render is
parsed and executed as a template again (double render), a user value that survived
the first pass as data becomes
{{ }}code on the second. Never re-parse rendered output; if a two-step render is unavoidable, escape/neutralise template metacharacters ({{,{%,${,#{,<%) in user fields before the second pass. - Use a sandboxed / logic-less engine for user-authored templates (Handlebars,
Liquid, Jinja2
SandboxedEnvironment) and deny attribute/builtin/internals access (__class__,__globals__,constructor,getClass()). - Keep autoescaping on for the correct context (HTML/JS/URL) — SSTI and XSS share the input path.
- Bound every loop and output driven by user input: cap iteration counts, recursion
depth, and total rendered size at the template boundary.
range(n)/{{range n}}with attacker-chosennand no cap is an OOM DoS.
NEVER
- Build a template's source string from user input (
Template("Hi " + user.name), f-strings /String.format/ concat feeding the compiler). Bind values instead. - Store a template body in a user-writable field (profile name, subject line, CMS block) and later feed it to the engine.
- Re-render engine output as a template without neutralising user-supplied metacharacters (double-render / "recursive template" pipelines).
- Expose the raw engine to untrusted authors without a sandbox and an attribute allowlist — SSTI escalates to RCE in most server engines (Jinja2, Twig, Freemarker, Velocity, ERB).
- Render an unbounded user-controlled loop / repetition / include count without a size and iteration cap.
KNOWN FALSE POSITIVES
- User data passed as context variables to a static template is the correct pattern, not SSTI.
- Client-side template binding (React/Vue/Angular interpolation) is XSS-scope, not
server SSTI (see frontend-security) — unless the template string itself is user-built
(
v-htmlof a template,new Function). - Logic-less engines (Mustache, strict Handlebars) with no user-authored template and autoescape on are low-risk for RCE (still cap output size).
- A template loaded from a trusted file whose values include user data is fine.
Context (for humans)
Server-Side Template Injection happens when user input is evaluated as part of a
template rather than passed as data to it. Because server engines expose rich object
access (attribute traversal, method calls, builtins), SSTI usually escalates from
information disclosure to remote code execution — {{7*7}} returning 49 is the classic
probe, {{''.__class__.__mro__[1].__subclasses__()}} the classic Jinja2 exploit.
Two variants are easy to miss in review:
- Double render. A pipeline renders once (often "safely", data-only), then the
result is parsed and executed as a template a second time — e.g. a body is built by
splicing a display name into a template string, and a dispatcher then
Parse()s andExecute()s that string. The name's{{...}}was inert data on pass one and executes on pass two. The fix is architectural: never re-parse rendered output, or filter template metacharacters from user fields before the re-parse. - Template-driven DoS. Even without RCE, a template is a denial-of-service sink: an
attacker-controlled loop /
range/includecount with no output cap makes one request allocate unbounded memory and OOM the renderer — and a crash mid-pipeline (before a queue offset commit) can crash-loop.
Verify & lock (triaging a finding)
A scanner/review hit is a candidate, not a confirmed bug. Confirm it, fix it, then lock it.
- Confirm it's real (probe the template boundary). Put a math/marker probe in the
user field that reaches the engine (
{{7*7}},${7*7},#{7*7},<%= 7*7 %>) and see if it evaluates (49) rather than echoing literally. For a double render, the probe must survive the first pass — inject it where the value is later re-parsed (a name/label spliced into a template source), then trigger the second render. For DoS, submit a large-but-bounded loop/rangecount and watch renderer memory/time; a real hit scales with the count with no cap. - Fix, then lock with a regression test: assert the probe input now renders as literal text (or is rejected), the sandbox denies attribute/builtins access, and an oversized loop/output is capped/rejected — plus a benign case (normal data in a static template) still renders. Commit it to CI so the guard can't be dropped in a later refactor.
References
rules/template_injection.json- OWASP WSTG — SSTI.
- PortSwigger — Server-Side Template Injection.
- CWE-1336 · CWE-94 · CWE-770.
What ships with it: 2 files
4.3 KB alongside SKILL.md
rules/
- template_injection.json2.4 KB
tests/
- corpus.json1.9 KB