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.
| 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
- 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)
- 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
});
- 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)
- Apply a Content Security Policy (CSP). Even if SSTI results in reflected XSS rather than RCE, a strict CSP limits the damage.
References
- CWE-95: Eval Injection
- OWASP Server-Side Template Injection
- PortSwigger: Server-Side Template Injection
- Jinja2 Sandbox documentation
- CAPEC-242: Code Injection
- MITRE ATT&CK T1059 – Command and Scripting Interpreter
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.