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

ImpactDetails
IntegrityScope: Integrity

Execute unauthorized code or commands affecting system integrity.
ConfidentialityScope: 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.

  • 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

  1. MITRE Corporation. "CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine." https://cwe.mitre.org/data/definitions/1336.html
  2. PortSwigger. "Server-Side Template Injection"
  3. OWASP. "Template Injection"