agentsclimarketplace

Template injection

Skill ShieldNet-360/secure-vibe/skills/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

Install
npx -y skills add ShieldNet-360/secure-vibe --skill template-injection

Assembled 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-chosen n and 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-html of 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:

  1. 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 and Execute()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.
  2. Template-driven DoS. Even without RCE, a template is a denial-of-service sink: an attacker-controlled loop / range / include count 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.

  1. 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/range count and watch renderer memory/time; a real hit scales with the count with no cap.
  2. 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

What ships with it: 2 files

4.3 KB alongside SKILL.md

tests/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.