Improper Neutralization of Special Elements Used in a Template Engine
Description
Improper Neutralization of Special Elements Used in a Template Engine occurs when products use template engines to process externally-influenced input without properly neutralizing special elements or syntax that template engines can interpret as code directives. Template engines (Twig, Jinja2, Pug, JSP, FreeMarker, Velocity, ColdFusion, Smarty, PHP) enable developers to insert external values into documents, but attackers can inject malicious expressions—such as "{{7*7}}"—to execute arbitrary code. Both server-side (SSTI) and client-side (CSTI) template injection variants exist.
Risk
Template injection has severe implications. Remote code execution possible. Server compromise. Data exfiltration. File system access. Arbitrary command execution. Cross-site scripting in client-side variants. Information disclosure. Service disruption. High likelihood when user input flows into templates without sanitization.
Solution
Select template engines offering sandbox or restricted modes during architecture and design phase. Implement available sandbox/restricted features during implementation phase. Never pass untrusted input directly into template expressions. Escape or sanitize user input before template processing. Use logic-less templates where possible.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Execute unauthorized code or commands affecting system integrity. |
| Confidentiality | Scope: Confidentiality Access to sensitive data through template expression evaluation. |
Example Code
Vulnerable Code
# Vulnerable: Python Jinja2 template injection
from flask import Flask, request, render_template_string
app = Flask(__name__)
# VULNERABLE: User input directly in template
@app.route('/greet')
def vulnerable_greet():
name = request.args.get('name', 'Guest')
# VULNERABLE: User input inserted into template string
template = f"<h1>Hello {name}!</h1>"
return render_template_string(template)
# Attack: ?name={{config}}
# Returns Flask configuration including SECRET_KEY
# Attack: ?name={{''.__class__.__mro__[1].__subclasses__()}}
# Lists all Python classes - path to RCE
# VULNERABLE: More severe Jinja2 injection
@app.route('/render')
def vulnerable_render():
user_template = request.args.get('template', '')
# VULNERABLE: User controls entire template
return render_template_string(user_template)
# Attack: ?template={{config.items()}}
# Attack: ?template={{''.class.__mro__[1].__subclasses__()[408]('id',shell=True,stdout=-1).communicate()}}
# Executes shell command 'id'
# VULNERABLE: Django template (less severe but still dangerous)
from django.template import Template, Context
def vulnerable_django_view(request):
user_input = request.GET.get('input', '')
# VULNERABLE: User input in template
template = Template(f"Hello {user_input}")
return template.render(Context({}))
// Vulnerable: Node.js template injection
const express = require('express');
const nunjucks = require('nunjucks');
const pug = require('pug');
const app = express();
// VULNERABLE: Nunjucks injection
app.get('/greet', (req, res) => {
const name = req.query.name || 'Guest';
// VULNERABLE: User input in template string
const template = `<h1>Hello ${name}!</h1>`;
const result = nunjucks.renderString(template);
res.send(result);
// Attack: ?name={{range(10)}}
// Attack: ?name={{''.__class__.__mro__[1].__subclasses__()}}
});
// VULNERABLE: Pug (Jade) template injection
app.get('/page', (req, res) => {
const title = req.query.title || 'Page';
// VULNERABLE: User input passed to Pug
const html = pug.render(`h1= title`, { title: title });
res.send(html);
// Could be vulnerable depending on how input is processed
});
// VULNERABLE: EJS injection
const ejs = require('ejs');
app.get('/render', (req, res) => {
const data = req.query.data || '';
// VULNERABLE: User input in EJS template
const template = `<div><%= data %></div>`;
const html = ejs.render(template, { data: data });
res.send(html);
// Attack patterns depend on specific EJS version
});
// VULNERABLE: Handlebars with helpers
const Handlebars = require('handlebars');
app.get('/helper', (req, res) => {
const template = req.query.template || 'Hello';
// VULNERABLE: User controls template
const compiled = Handlebars.compile(template);
res.send(compiled({}));
});
// Vulnerable: Java template injection
import freemarker.template.*;
import org.apache.velocity.*;
public class VulnerableTemplates {
// VULNERABLE: FreeMarker injection
public String vulnerableFreeMarker(String userInput) throws Exception {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
// VULNERABLE: User input in template string
String templateStr = "Hello " + userInput;
Template template = new Template("temp", new StringReader(templateStr), cfg);
StringWriter out = new StringWriter();
template.process(new HashMap<>(), out);
return out.toString();
// Attack: ${7*7} -> 49
// Attack: ${"freemarker.template.utility.Execute"?new()("id")}
// Executes shell command
}
// VULNERABLE: Apache Velocity injection
public String vulnerableVelocity(String userInput) {
VelocityEngine ve = new VelocityEngine();
ve.init();
// VULNERABLE: User input in Velocity template
String templateStr = "Hello " + userInput;
VelocityContext context = new VelocityContext();
StringWriter writer = new StringWriter();
ve.evaluate(context, writer, "temp", templateStr);
return writer.toString();
// Attack: #set($x='')#set($rt=$x.class.forName('java.lang.Runtime'))
// Attack: $class.inspect("java.lang.Runtime").type.getRuntime().exec("id")
}
// VULNERABLE: Thymeleaf injection
public String vulnerableThymeleaf(String userInput) {
// When using Thymeleaf with expression preprocessing
// [[${userInput}]] can be vulnerable
// Attack: __${T(java.lang.Runtime).getRuntime().exec('id')}__
return "";
}
}
Fixed Code
# Fixed: Safe Python template handling
from flask import Flask, request, render_template_string, escape
from markupsafe import Markup
import jinja2
app = Flask(__name__)
# FIXED: Escape user input
@app.route('/greet')
def safe_greet():
name = request.args.get('name', 'Guest')
# FIXED: Escape user input before inserting in template
safe_name = escape(name)
template = f"<h1>Hello {safe_name}!</h1>"
return template # Not using render_template_string at all
# FIXED: Use proper template with variables
@app.route('/greet_safe')
def safe_greet_proper():
name = request.args.get('name', 'Guest')
# FIXED: Pass data as context, not in template string
return render_template_string(
"<h1>Hello {{ name }}!</h1>",
name=name # Jinja2 auto-escapes in HTML context
)
# FIXED: Sandboxed Jinja2 environment
from jinja2.sandbox import SandboxedEnvironment
def safe_render_sandboxed(template_str, context):
# FIXED: Use sandbox to restrict template capabilities
env = SandboxedEnvironment()
# FIXED: Disable dangerous features
env.globals = {} # No global functions
try:
template = env.from_string(template_str)
return template.render(context)
except jinja2.exceptions.SecurityError:
return "Template security violation"
# FIXED: Whitelist allowed template syntax
import re
def safe_render_restricted(user_template, context):
# FIXED: Only allow simple variable substitution
# Reject any template syntax that could be dangerous
# Block dangerous patterns
dangerous_patterns = [
r'\{\{.*\.__', # Attribute access
r'\{\{.*\[', # Item access
r'\{\{.*\(', # Function calls
r'\{%', # Template tags
r'config', # Flask config
r'request', # Flask request
r'self', # Self reference
]
for pattern in dangerous_patterns:
if re.search(pattern, user_template, re.IGNORECASE):
raise ValueError("Dangerous template pattern detected")
# Only allow simple {{ variable }} syntax
allowed_pattern = r'\{\{\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\}\}'
if re.sub(allowed_pattern, '', user_template).find('{{') != -1:
raise ValueError("Complex template expressions not allowed")
env = SandboxedEnvironment()
template = env.from_string(user_template)
return template.render(context)
// Fixed: Safe Node.js template handling
const express = require('express');
const nunjucks = require('nunjucks');
const xss = require('xss');
const app = express();
// FIXED: Configure Nunjucks with auto-escaping
const env = nunjucks.configure('views', {
autoescape: true, // FIXED: Auto-escape output
express: app
});
// FIXED: Use template files, not inline strings
app.get('/greet', (req, res) => {
const name = req.query.name || 'Guest';
// FIXED: Render from file with auto-escaped context
res.render('greet.html', { name: name });
// In greet.html: <h1>Hello {{ name }}!</h1>
// Nunjucks auto-escapes the name variable
});
// FIXED: Escape when building dynamic templates
app.get('/dynamic', (req, res) => {
const name = req.query.name || 'Guest';
// FIXED: Escape user input manually
const safeName = xss(name);
// FIXED: Don't use user input in template string at all
const html = `<h1>Hello ${safeName}!</h1>`;
res.send(html); // Plain string, not template
});
// FIXED: Validate and sanitize template input
function safeRenderTemplate(templateStr, context) {
// FIXED: Whitelist allowed syntax
const dangerousPatterns = [
/\{\{.*\.__/, // Attribute access
/\{\{.*\[.*\]/, // Array/object access
/\{\{.*\(.*\)/, // Function calls
/\{%/, // Block tags
/import\s/, // Import statements
/require\s*\(/, // Require calls
];
for (const pattern of dangerousPatterns) {
if (pattern.test(templateStr)) {
throw new Error('Dangerous template pattern');
}
}
// FIXED: Use safe subset of Nunjucks
const safeEnv = new nunjucks.Environment(null, {
autoescape: true
});
// FIXED: Remove all globals and filters
safeEnv.globals = {};
return safeEnv.renderString(templateStr, context);
}
// FIXED: Use logic-less templates (Mustache)
const Mustache = require('mustache');
app.get('/safe', (req, res) => {
const name = req.query.name || 'Guest';
// FIXED: Mustache is logic-less and safer
const template = '<h1>Hello {{name}}!</h1>';
const html = Mustache.render(template, { name: name });
res.send(html);
// Mustache escapes by default and has no code execution
});
// Fixed: Safe Java template handling
import freemarker.template.*;
import freemarker.core.TemplateClassResolver;
public class SafeTemplates {
// FIXED: Secure FreeMarker configuration
public Configuration getSecureFreemarkerConfig() {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
// FIXED: Disable dangerous built-ins
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
// FIXED: Disable ?new and ?api
cfg.setAPIBuiltinEnabled(false);
// FIXED: Set strict syntax mode
cfg.setTagSyntax(Configuration.AUTO_DETECT_TAG_SYNTAX);
return cfg;
}
// FIXED: Safe FreeMarker rendering
public String safeFreeMarker(String userInput) throws Exception {
Configuration cfg = getSecureFreemarkerConfig();
// FIXED: Use template file, pass input as data
Map<String, Object> data = new HashMap<>();
data.put("name", userInput); // FreeMarker escapes in HTML
Template template = cfg.getTemplate("greeting.ftl");
StringWriter out = new StringWriter();
template.process(data, out);
return out.toString();
}
// FIXED: Input validation before template
public String safeRenderWithValidation(String userInput) throws Exception {
// FIXED: Reject any template syntax in input
if (userInput.contains("${") || userInput.contains("#{") ||
userInput.contains("<#") || userInput.contains("[#")) {
throw new IllegalArgumentException("Template syntax not allowed in input");
}
// FIXED: Escape special characters
String escaped = StringEscapeUtils.escapeHtml4(userInput);
Configuration cfg = getSecureFreemarkerConfig();
Map<String, Object> data = new HashMap<>();
data.put("name", escaped);
Template template = cfg.getTemplate("greeting.ftl");
StringWriter out = new StringWriter();
template.process(data, out);
return out.toString();
}
// FIXED: Use whitelist for allowed variables
public String safeTemplateWithWhitelist(String templateStr, Map<String, Object> context) {
Set<String> allowedVariables = Set.of("name", "title", "message");
// FIXED: Only allow whitelisted variable references
Pattern varPattern = Pattern.compile("\\$\\{\\s*([a-zA-Z_]+)\\s*\\}");
Matcher matcher = varPattern.matcher(templateStr);
while (matcher.find()) {
String varName = matcher.group(1);
if (!allowedVariables.contains(varName)) {
throw new IllegalArgumentException("Variable not allowed: " + varName);
}
}
// Additional check for any other FreeMarker syntax
if (templateStr.matches(".*(<#|\\[#|\\?\\w+).*")) {
throw new IllegalArgumentException("Advanced template syntax not allowed");
}
// Proceed with safe rendering
// ...
}
}
CVE Examples
- CVE-2024-34359: Jinja2 prompt injection in LLM application.
- CVE-2020-12790: Twig template injection allowing remote code execution.
- CVE-2020-4027: Apache Velocity macro bypass enabling code execution.
- CVE-2019-10744: Lodash template injection leading to code execution.
Related CWEs
- CWE-94: Improper Control of Generation of Code ('Code Injection') (parent)
- CWE-917: Improper Neutralization of Special Elements used in an Expression Language Statement ('Expression Language Injection') (peer)
- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (related)
References
- MITRE Corporation. "CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine." https://cwe.mitre.org/data/definitions/1336.html
- PortSwigger. "Server-Side Template Injection"
- OWASP. "Template Injection"