User input reaching a template compiler rather than its context: the source-versus-values distinction, double-render pipelines, template names chosen by the caller, why a template sandbox is a mitigation and not a boundary, minimizing what the render context exposes, and template-driven resource exhaustion. Use when rendering templates with user-influenced values, when a template string or template name is assembled from input, or when a product lets users author templates.
Installs into .claude/skills of the current project.
Are you the author of Template Injection?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/shieldnet-360-template-injection-secure-vibe)
---
name: template-injection
description: "User input reaching a template compiler rather than its context: the source-versus-values distinction, double-render pipelines, template names chosen by the caller, why a template sandbox is a mitigation and not a boundary, minimizing what the render context exposes, and template-driven resource exhaustion. Use when rendering templates with user-influenced values, when a template string or template name is assembled from input, or when a product lets users author templates."
---
<!-- Native skill bundle for Claude Code. Generated by `secure-vibe dev regenerate`. -->
<!-- Do not edit by hand; the source of truth is skills/template-injection/SKILL.md. -->
# Template Injection (SSTI)
User input reaching a template compiler rather than its context: the source-versus-values distinction, double-render pipelines, template names chosen by the caller, why a template sandbox is a mitigation and not a boundary, minimizing what the render context exposes, and template-driven resource exhaustion. Use when rendering templates with user-influenced values, when a template string or template name is assembled from input, or when a product lets users author templates.
## ALWAYS
- Pass user data as **bound values**, never into the template **source** string. This is the whole skill in one line: `render(tmpl, {"name": name})` is safe, `render("Hello " + name)` is SSTI. The compiler treats its source as code, and the context as data — so the only question that matters about any user value is which of the two it reaches.
- Keep the set of template sources **static and trusted**, loaded from files or constants. A template body read from a database column, an API response, or a request field is attacker-supplied code, whatever the column is called.
- Choose the template **by name from an allowlist**, never by a name the caller supplies. `render_template(user_value)` and `{% include user_value %}` are the same vulnerability as a user-supplied body: the caller picks which code runs, and on many engines the name resolves as a filesystem path, which makes it an arbitrary-file read as well.
- **Minimize the render context.** Pass the specific values the template needs, never the ambient objects — application config, settings, the request, `self`, a module, a live ORM instance. This is the control that still holds when a sandbox is escaped: an engine that cannot execute code can still print whatever it can reach, and configuration objects hold database URLs and signing keys.
- Treat a **second render pass** as compilation of whatever survived the first. If rendered output is parsed again, a user value that was inert data on pass one is `{{ }}` code on pass two. The fix is architectural — stop re-parsing rendered output. Neutralizing metacharacters before the second pass is a fallback that has to enumerate every delimiter of every engine, including ones a custom delimiter configuration introduces, and two adjacent fields can produce a delimiter that neither of them contains.
- **Bound the render**: cap iteration counts, recursion depth, include depth, output size, and wall-clock time. A template is a denial-of-service sink even where it is not a code-execution one, and an unbounded loop count is one request that allocates until the process dies.
- Keep **autoescaping on**, and know that autoescape is an XSS control rather than an SSTI one — it escapes what the template prints, not what the template *is*. `frontend-security` owns output encoding and the raw-output opt-outs (`|safe`, `{{{ }}}`, `template.HTML`) that turn it off per value.
## NEVER
- Build a template's source from user input — string concatenation, an f-string, `String.format`, or a `Template(...)` / `from_string` / `compile` call whose argument contains a value you did not write.
- Store a template body in a user-writable field — a profile name, an email subject, a CMS block — and later hand it to the engine.
- Treat a **sandbox as a trust boundary**. Sandboxed environments restrict attribute and builtin access, and they have a documented history of escapes in every engine that offers one; the escape is usually a way to reach an ordinary-looking attribute that leads back to a callable. A sandbox lowers the probability of RCE and does not make user-authored templates safe. If the product genuinely requires user-authored templates, isolate the renderer — separate process, no network, no filesystem, no credentials, a wall-clock timeout — and keep the engine patched.
- Enumerate forbidden attribute names as the control (`__class__`, `__globals__`, `constructor`, `getClass`). A denylist of literal names is defeated by string concatenation, indexing, and dynamic attribute lookup. Allowlist what the template may reach instead.
- Assume the client-side framework is out of scope when the *template string itself* is user-built. Runtime template compilation from user input — a compiler-included build compiling a user string, or `new Function` — is injection on the client. Interpolating a user value into a normal component template is XSS, which `frontend-security` owns.
## KNOWN FALSE POSITIVES
- User data passed as **context variables** to a static template is the correct pattern. This is the shape the whole skill is pointing at, not a finding.
- A `{{ … }}` or `${ … }` sequence in a string that never reaches a compiler is not SSTI: a client-side template shipped as a server-side string literal, an i18n catalogue, a Helm / Kubernetes / Grafana / Prometheus manifest, a CI expression. The finding requires a render call, not a delimiter.
- A logic-less engine with no user-authored template is not an RCE risk, though it is still subject to the output-size and XSS rules. Note that being *called* logic-less is not the same as being logic-less — engines that support helpers, partials and subexpressions are not, whatever their marketing says.
- Escaping opt-outs applied to content the application itself generated — a rendered Markdown block, a sanitized HTML fragment — are a `frontend-security` question about the sanitizer, not an SSTI finding.
## Reference files
Read these only when the task calls for them.
- `references/engines.md`
- `references/verifying-findings.md`