Skip to content

VNX-95: Template Injection (Eval Injection)

HTTP request data becomes the source of a Jinja2, Mako, Django, EJS, Pug, Handlebars, Nunjucks, lodash, ERB, Erubi, Haml, Slim or Rails render inline: template, directly or through a variable.

Updated 02 Nov 2026Checked against cli@de9b52a
Field Value
Rule ID VNX-95
Severity Critical
SARIF level error
Kind sast
Languages JavaScript / TypeScript, Python, Ruby
CWE CWE-95
CAPEC CAPEC-242
ATT&CK T1059
CWSS vector CWSS:1.0/TI:C/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:C/DI:H/EX:H/EC:N/P:W
Tags template-injection ssti eval-injection cwe-95
Source internal/sast/rules/vnx-95.rego

Overview

This rule flags server-side template engine calls, Jinja2’s render_template_string(), Mako’s Template(), Node.js Pug/EJS/Handlebars compile/render functions, and Ruby’s ERB.new(), Erubi, Haml, Slim and Rails render inline:, whose template source (first argument) is HTTP request data, used directly or through a variable assigned from it in the 25 lines above. Request data passed as template context is not reported. Server-Side Template Injection (SSTI) allows an attacker to inject template directives that are evaluated by the template engine with full application privileges, often leading to arbitrary code execution. This maps to CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code (‘Eval Injection’).

Why This Matters

SSTI is frequently confused with XSS, but the consequences are far worse: instead of JavaScript executing in a visitor’s browser, arbitrary code executes on the server. In Jinja2, the payload {{''.__class__.__mro__[1].__subclasses__()}} enumerates all Python classes; from there an attacker can reach subprocess and execute OS commands. In Pug (formerly Jade), - prefix lines execute arbitrary JavaScript. The vulnerability is subtle because the same template engine that is safe when rendering render_template('page.html', name=name) becomes catastrophically dangerous when the template source itself comes from user input via render_template_string(name).

What Gets Flagged

The template SOURCE (first argument) carries request data, directly or through a variable assigned from it:

# FLAGGED
name = request.args.get("name", "guest")
page = "<h1>Hello " + name + "</h1>"
return render_template_string(page)
tpl = env.from_string(request.form["body"])
// FLAGGED
res.send(ejs.render(req.query.tpl, {}));
const t = Handlebars.compile(`<div>${req.body.msg}</div>`);
# FLAGGED
html = ERB.new(params[:template]).result(binding)
render inline: "<p>#{params[:message]}</p>"

Not flagged: request data passed as context (render_template_string("<h1>{{ name }}</h1>", name=name), ejs.render(layout, { name: req.query.name })), templates read from files, and Python string.Template.

Remediation

  1. Never use render_template_string() or equivalent with user input. Pass user data as context variables to a static template file.
# SAFE: Flask/Jinja2, static template file, user data as context
from flask import render_template

@app.route('/greet')
def greet():
    name = request.args.get('name', '')
    # name is passed as a variable, not as part of the template syntax
    return render_template('greet.html', name=name)
    # greet.html: <h1>Hello {{ name }}</h1>, Jinja2 auto-escapes this
# SAFE: Jinja2 sandbox for when dynamic templates are genuinely required
from jinja2.sandbox import SandboxedEnvironment

env = SandboxedEnvironment()
# SandboxedEnvironment restricts access to Python internals,
# but still do not pass user data as the template source if avoidable.
template = env.from_string(developer_controlled_template)
output = template.render(user_data=safe_context_dict)
  1. For Node.js template engines, precompile from static files.
// SAFE: Pug, compile from a static file
const pug = require('pug');
const compiledFn = pug.compileFile('views/greet.pug');

app.get('/greet', (req, res) => {
    res.send(compiledFn({ name: req.query.name }));  // name is a variable, not template
});
// SAFE: EJS, render from a file
app.get('/preview', (req, res) => {
    res.render('preview', { user: req.user, data: sanitizedData });
    // views/preview.ejs accesses <%= user.name %>, EJS escapes by default
});
  1. For Ruby ERB, load templates from disk.
# SAFE: Load template from a controlled file path
template = ERB.new(File.read(Rails.root.join('app/views/mail/template.html.erb')))
output = template.result_with_hash(name: safe_name)
  1. Apply a Content Security Policy (CSP). Even if SSTI results in reflected XSS rather than RCE, a strict CSP limits the damage.

References

Suppressing this rule

Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):

# nosec vnx-95
// nosec vnx-95

Or record an ignore rule: vulnetix ignore add --rule vnx-95 --reason "...". See Suppressing findings.

Edit this pageSources: cli/internal/sast/rules/vnx-95.rego, cli/internal/sast/docs/vnx-95.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.