Klassisches XSS schmuggelt JavaScript in HTML-Output. Server-Side Template Injection (SSTI) geht eine Schicht tiefer: User-Input wird nicht in ein Template eingefügt (mit Encoding), sondern als Template selbst ausgewertet. Wer in einer Jinja2- oder Twig-Anwendung einen Template-Ausdruck unterbringt, kann auf interne Objekte zugreifen — und in vielen Fällen direkt Code im Server-Prozess ausführen. SSTI ist seit 2015 als eigenständige Klasse bekannt; reale Vorfälle reichen bis zu kritischen RCEs in Unternehmens-Apps.
Was SSTI ist — und was nicht
Template-Engines wie Jinja2, ERB, Twig, Handlebars, Mustache, FreeMarker, Velocity haben eine Template-Sprache mit Syntax wie {{ variable }} oder <%= variable %>. Normalerweise:
- Template wird vom Entwickler geschrieben (vertraut).
- Daten kommen vom Nutzer (untrusted).
- Template-Engine fügt die Daten als Werte ein, optional mit Encoding.
Sicheres Muster:
from jinja2 import Template
# Template ist statisch — vom Entwickler geschrieben
template = Template("Hallo, {{ name }}!")
# Daten kommen von Nutzer-Input
result = template.render(name=request.form['name'])
# Wenn name "{{ 7*7 }}" ist, wird es als TEXT ausgegeben — kein Code
# Ergebnis: "Hallo, {{ 7*7 }}!"SSTI passiert, wenn diese Trennung verletzt wird:
# Schadhaft: User-Input wird als Template-Code ausgewertet
template_str = f"Hallo, {request.form['name']}!"
template = Template(template_str)
result = template.render()
# Wenn name "{{ 7*7 }}" ist, wertet Jinja den Ausdruck aus
# Ergebnis: "Hallo, 49!"Sobald der/die Angreifer:in {{ 7*7 }} sieht, weiß sie: es ist Jinja, und sie kann Template-Ausdrücke ausführen. Von dort ist es oft ein kurzer Weg zur RCE.
Wichtiger Unterschied zu XSS:
XSS läuft im Browser der Nutzer:in. SSTI läuft auf dem Server. Die Wirkung reicht potenziell bis zu Remote Code Execution im Server-Prozess — mit Zugriff auf Dateisystem, interne Services, Cloud-Credentials.
Wo SSTI typisch entsteht
SSTI passiert, wenn User-Input in die Template-Definition fließt, statt nur in die Daten:
- Mail-Templates aus User-Input — Marketing-Tool, das Nutzer:innen erlaubt, eigene Templates anzulegen.
- CMS-Workflows — Editor erlaubt Template-Syntax in Inhalts-Feldern.
- Error-Pages mit reflektiertem Inhalt —
f"Error: {user_error_msg}"als Template gerendert. - Konfigurations-Files aus User-Datenbank — wenn Konfig per Template evaluiert wird.
- Slack-/Discord-Bots mit Template-Variablen im Antwort-Pfad.
- Workflow-Engines (ServiceNow, Atlassian, Jenkins-Pipelines) — wenn Pipeline-Definitionen User-editierbar sind.
- PDF-Generatoren mit User-konfigurierbaren Templates.
- Reporting-Tools mit User-definierten Formeln.
Eine typische Stelle: „Personalisierte Begrüßung" oder „Custom Email"-Feature, wo Nutzer:innen Templates schreiben dürfen. Wenn die Template-Engine ohne Sandbox läuft, ist SSTI quasi garantiert.
Detection: SSTI in 60 Sekunden finden
Drei klassische Detection-Payloads:
Math-Test: {{ 7*7 }} → ergibt 49 bei Jinja2/Twig/Handlebars (in evaluate-Modus).
ERB-Test: <%= 7*7 %> → ergibt 49 bei Ruby ERB.
Velocity-/FreeMarker-Test: ${7*7} → ergibt 49.
Wenn eine dieser Eingaben 49 statt der Eingabe selbst zurückgibt, ist SSTI bestätigt. Welcher Engine? — der nächste Schritt ist Fingerprinting:
| Payload | Engine | Charakteristisch |
|---|---|---|
{{ 7*7 }} → 49 | Jinja2, Twig, Django-Templates, Liquid | Verbreitet in Python/PHP |
<%= 7*7 %> → 49 | ERB, EJS | Ruby, Node.js |
${7*7} → 49 | Velocity, FreeMarker, Spring-EL | Java |
{{7*7}} → 49, dann {{ self.__init__.__globals__ }} → tiefer Zugriff | Jinja2 spezifisch | Python |
{{ config.items() }} → Config-Dictionary | Flask + Jinja2 | Python-Flask |
${product.__class__} → Java-Klassen-Pfad | FreeMarker | Java |
Sobald der Engine bekannt ist, folgt Eskalation zu RCE durch engine-spezifische Patterns.
Eskalations-Pfade pro Engine
Jinja2 (Python).
Jinja2 hat zwei Modi: Sandbox-Modus (sicherer) und Normal-Modus (RCE möglich). Klassische Eskalation:
# Eskalation: Zugriff auf Python-Globale über Object-Inheritance
{{ ''.__class__.__mro__[1].__subclasses__() }}
# Listet alle geladenen Python-Klassen
# RCE via subprocess
{{ ''.__class__.__mro__[1].__subclasses__()[200].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("id").read()') }}Exakte Indizes sind Python-Version-spezifisch; Angreifer:innen probieren systematisch durch.
Twig (PHP).
Twig hat ähnliche Eskalations-Pfade über Filter-Aufrufe:
{{ ['id'] | filter('system') }}
# Ruft system('id') auf — RCE
{{ 'system'(' id') }}
# Alternative Syntax (engine-version-abhängig)Smarty (PHP).
Smarty 3+ hat Sandbox-Modus per Default. Aber: {php}-Tag (deprecated, aber in alten Versionen vorhanden) erlaubt direktes PHP-Code-Embedding.
FreeMarker (Java).
<#assign value="freemarker.template.utility.Execute"?new()>
${value("id")}
# Java-Runtime-ExecVelocity (Java).
#set($x="")
$x.class.forName("java.lang.Runtime").getRuntime().exec("id")Handlebars (JavaScript).
Handlebars hat keinen direkten Code-Eval — aber {{#with}}-Helper plus Konstruktor-Tricks können in alten Versionen zu RCE führen.
Die meisten Engines haben CVE-Wellen durchgemacht. Bug-Bounty-Reports auf HackerOne mit Filter „SSTI" zeigen viele konkrete Eskalations-Pfade.
Sandbox-Modus: hilft, ist aber kein Allheilmittel
Mehrere Engines bieten Sandbox-Modi, die gefährliche Operationen einschränken:
Jinja2 SandboxedEnvironment:
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
template = env.from_string(user_template)
result = template.render(data=...)
# Manche RCE-Pfade sind blockiert (Inheritance-Tricks, dangerous attrs)Aber: Sandbox-Escapes sind möglich und werden regelmäßig publiziert. Beispiele:
- Jinja2 Sandbox Bypass via String-Methoden (CVE-2016-10745, CVE-2019-10906).
- Twig Sandbox Bypass via Filter-Verkettung (mehrere CVEs).
- Spring SpEL Sandbox Bypass (CVE-2022-22963, CVE-2022-22965 = Spring4Shell, weitere).
Konsequenz: Sandbox ist eine Verteidigungs-Schicht, nicht eine vollständige Lösung. Die richtige Antwort ist keine User-Input-Templates, nicht „besseres Sandboxing".
Die richtige Lösung: Trennung von Template und Daten
Templates sollten immer vom Entwickler geschrieben werden — nie vom Nutzer. Daten sollten nur als Variablen in das Template gelangen, nie als Template-Code.
Schadhaft:
# User-Input wird Teil der Template-Definition
body = user_email_template # vom Nutzer
template = Template(body) # SSTI möglich
result = template.render(user=user)Sicher:
# Template ist vom Entwickler — User-Input nur als Variable
body_template = """
Hallo, {{ name }}!
{{ message }}
"""
template = Template(body_template)
result = template.render(
name=user.name,
message=user_provided_text # als Daten, nicht als Template
)Wenn User-Templates wirklich nötig sind:
- Eigene, minimale Template-Sprache bauen — nur Variablen-Substitution, kein Code-Eval. Beispiel:
format-Strings in Python ("Hallo, {name}!".format(name=user_name)) ohne f-String-Eval-Eigenschaften. - Strict-Allowlist der erlaubten Template-Features (z. B. nur Variablen und if/else, keine Funktions-Aufrufe).
- Sandbox-Engine mit zusätzlicher Code-Review.
- Isolierter Prozess für Template-Rendering — Ergebnis als String, kein Zugriff auf Hauptprozess.
Wenn User-Inhalt sicher gerendert werden soll:
- Markdown-Renderer mit Sanitization (siehe html-sanitization).
- BBCode oder eigene begrenzte Markup-Sprachen.
- Strukturierte Editoren (TipTap, ProseMirror) statt Roh-Template-Eingabe.
Verwandte Klassen: Client-Side Template Injection (CSTI)
Manche Client-Side-Frameworks (älteres AngularJS) hatten ähnliche Probleme im Browser. AngularJS 1.x evaluiert {{ ... }}-Ausdrücke client-seitig — wenn User-Input ungesäubert in ein gerendertes Template fließt, kann Angular-Sandbox-Bypass zu DOM-XSS führen.
AngularJS-Beispiel (deprecated, nur historisch):
<!-- Schadhaft mit AngularJS 1.x -->
<div ng-app>
User: {{ userInput }}
</div>
<!-- userInput = "{{ constructor.constructor('alert(1)')() }}"
eskaliert zu Code-Execution -->AngularJS hatte mehrere Sandbox-Modi, die alle umgehbar waren. Die Lösung war: AngularJS auf Angular 2+ migrieren, das diese Klasse strukturell nicht mehr hat.
In modernen Frameworks (React, Vue, Angular 2+, Svelte) ist CSTI selten — die Template-Engines kompilieren strikt, User-Input wird per Default als Daten behandelt. Aber: dangerouslySetInnerHTML und Äquivalente können in seltenen Fällen CSTI-ähnliche Effekte haben, wenn Markdown/HTML-Renderer User-Eingaben als Template behandeln.
Reale SSTI-Vorfälle
Uber RCE 2016 — Spring-EL-SSTI in einer internen Microservice-API, Bug-Bounty-Auszahlung im fünfstelligen Bereich.
Confluence (Atlassian) — mehrere SSTI-CVEs in OGNL-Expression-Verarbeitung (z. B. CVE-2022-26134), die zu RCE führten und massiv ausgenutzt wurden.
Apache Struts2 — wiederholte OGNL-Injection-CVEs, darunter CVE-2017-5638 (genutzt für den Equifax-Datenleck mit 147 Millionen betroffenen Datensätzen).
Spring4Shell (CVE-2022-22965) — Spring-Framework, durch ClassLoader-Manipulation via Property-Binding zu RCE; eine Art SSTI-nahe Klasse.
Bug-Bounty-Reports auf HackerOne und Bugcrowd zeigen regelmäßig SSTI-Funde bei großen Anbietern — meist in Custom-Mail-Templates, in Workflow-Engines oder in Self-Service-Konfigurations-Tools.
SSTI ist eine High-Severity-Klasse: bei Erfolg meist RCE, mit allen Folgen — Datenleck, Lateral Movement, Persistenz im Server.
Besonderheiten
James Kettle hat SSTI 2015 systematisch erforscht
Die Veröffentlichung „Server-Side Template Injection" (2015, PortSwigger Research) hat die Klasse als eigenständig etabliert. Davor wurden solche Bugs als „Eval Injection" oder ähnlich kategorisiert. Kettle hat eine systematische Detection-Methodik dokumentiert.
Tplmap als spezialisiertes Tool
Tplmap ist ein Open-Source-Tool zur SSTI-Detection und -Exploitation. Funktioniert ähnlich wie sqlmap für SQL-Injection — testet automatisch verschiedene Engines, eskaliert zu RCE wo möglich. Bug-Bounty-Hunter:innen nutzen es regelmäßig.
Sandboxes sind kein Endziel
Die meisten Template-Engines haben Sandbox-Modi — und die meisten Sandboxes haben dokumentierte Bypasses. Wer eine User-Input-Template-Funktion baut, sollte nicht auf „besseres Sandboxing" vertrauen. Die strukturelle Antwort ist Trennung von Template und Daten, nicht Sandbox-Polish.
OGNL ist eine eigene SSTI-Welle
Object-Graph Navigation Language (OGNL) in Apache Struts hat über die Jahre eigene CVE-Welle erzeugt — Struts2-OGNL-Injection ist die häufigste Java-Web-RCE-Klasse. Ähnlich: Spring SpEL (Spring Expression Language) hat mehrere CVEs. Wer Java/Web baut, sollte die OGNL/SpEL-Geschichte kennen.
Confluence- und Jira-Wellen 2022/23
Atlassian-Produkte (Confluence, Jira) hatten 2022–2023 mehrere OGNL-Injection-CVEs, die zu RCE führten — CVE-2022-26134 (Confluence), CVE-2022-1471 (SnakeYAML), CVE-2023-22515 (Confluence) und weitere. Klassische SSTI-Klasse mit großer Reichweite, weil Confluence in Tausenden Unternehmen läuft.
Mail-Template-Editoren als Risiko-Klasse
Mailchimp-ähnliche Funktionen — „Schreibe dein eigenes Mail-Template mit Variablen" — sind klassische SSTI-Stellen. Wenn der Anbieter Jinja2 / Twig / Handlebars im normalen Modus nutzt, ist SSTI quasi garantiert. Lösung: minimale eigene Variablen-Substitution statt vollwertige Template-Engine.
Cloud-Function-Templates und Infrastructure-as-Code
Manche IaC-Tools (Terraform, CloudFormation, Ansible) erlauben Template-Syntax in Konfigurationen. Wenn User-Input darin landet (Self-Service-Portale), kann das SSTI-ähnliche Effekte haben — manchmal mit Folge bis zu Cloud-Resource-Manipulation.
Weiterführende Ressourcen
Externe Quellen
- PortSwigger Research — Server-Side Template Injection
- PortSwigger Web Security Academy — SSTI
- Tplmap — SSTI Detection & Exploitation
- OWASP SSTI Prevention Cheat Sheet
- PayloadsAllTheThings — SSTI
- Jinja2 Sandbox Documentation
- Twig Sandbox Documentation
- OWASP — SSTI Attack