Quiz : pouvez-vous vous défendre contre la vulnérabilité XSS ?
Testez vos connaissances dans le quiz de sécurité ! Pouvez-vous empêcher un attaquant de prendre le contrôle d'une page HTML ?

Dans toutes les tâches, vous résoudrez la même question : comment afficher
correctement la variable $str
dans une page HTML pour éviter de
créer une vulnérabilité
XSS. La base de la défense est l'échappement, ce qui signifie
remplacer les caractères ayant une signification spéciale par les séquences
correspondantes. Par exemple, lors de l'affichage d'une chaîne dans du texte
HTML, où le caractère <
a une signification spéciale (il
signale le début d'une balise), nous le remplaçons par l'entité HTML
<
et le navigateur affichera correctement le symbole
<
.
Soyez vigilant, car la vulnérabilité XSS est très grave. Elle peut permettre à un attaquant de prendre le contrôle de la page ou même du compte utilisateur. Bonne chance et que vous réussissiez à maintenir votre page HTML en sécurité !
Première série de trois questions
Indiquez quels caractères et de quelle manière doivent être traités dans le premier, deuxième et troisième exemple :
1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>
Si la sortie n'était pas traitée, elle ferait partie de la page affichée.
Si un attaquant insérait la chaîne 'foo" onclick="evilCode()'
dans la variable et que la sortie n'était pas traitée, cela entraînerait
l'exécution de son code lors d'un clic sur l'élément :
$str = 'foo" onclick="evilCode()'
❌ sans traitement : <input value="foo" onclick="evilCode()">
✅ avec traitement : <input value="foo" onclick="evilCode()">
Solution des exemples individuels :
- les caractères
<
et&
représentent le début d'une balise HTML et d'une entité, nous les remplaçons par<
et&
- les caractères
"
et&
représentent la fin de la valeur de l'attribut et le début d'une entité HTML, nous les remplaçons par"
et&
- les caractères
'
et&
représentent la fin de la valeur de l'attribut et le début d'une entité HTML, nous les remplaçons par'
et&
Pour chaque bonne réponse, vous obtenez un point. Bien sûr, dans les trois cas, il est possible de remplacer également d'autres caractères par des entités, cela ne pose aucun problème, mais ce n'est pas nécessaire.
Question n° 4
Continuons. Quels caractères faut-il remplacer lors de l'affichage de la variable dans ce contexte ?
<input value=<?= $str ?>>
Solution : Comme vous pouvez le voir, les guillemets manquent ici. Le plus
simple est simplement d'ajouter les guillemets, puis d'échapper comme dans la
question précédente. Il existe une deuxième solution, qui consiste à
remplacer dans la chaîne l'espace et tous les caractères ayant une
signification spéciale à l'intérieur de la balise par des entités HTML,
c'est-à-dire >
, /
, =
et certains
autres.
Question n° 5
Maintenant, ça commence à devenir intéressant. Quels caractères faut-il traiter dans ce contexte :
<script>
let foo = '<?= $str ?>';
</script>
Solution : À l'intérieur de la balise <script>
, les
règles d'échappement sont déterminées par JavaScript. Les entités HTML ne
sont pas utilisées ici, mais une règle spéciale s'applique. Alors,
quels caractères échappons-nous ? À l'intérieur d'une chaîne JavaScript,
nous échappons bien sûr le caractère '
qui la délimite, et ce
à l'aide d'une barre oblique inverse, nous le remplaçons donc par
\'
. Comme JavaScript ne prend pas en charge les chaînes
multilignes (sauf en tant que template
literal), nous devons également échapper les caractères de fin de ligne.
Attention cependant, outre les caractères habituels \n
et
\r
, JavaScript considère également les caractères unicode
\u2028
et \u2029
comme des fins de ligne, que nous
devons également échapper. Et enfin, la règle spéciale mentionnée : la
chaîne ne doit pas contenir </script
. On peut éviter cela, par
exemple, en le remplaçant par <\/script
.
Si vous saviez cela, félicitations.
Question n° 6
Le contexte suivant ne semble être qu'une variation du précédent. Pensez-vous que le traitement sera différent ?
<p onclick="foo('<?= $str ?>')"></p>
Solution : Ici aussi, les règles d'échappement dans les chaînes JavaScript
s'appliquent, mais contrairement au contexte précédent où l'échappement à
l'aide d'entités HTML n'était pas utilisé, ici au contraire, il est utilisé.
Donc, nous échappons d'abord la chaîne JavaScript à l'aide de barres obliques
inverses, puis nous remplaçons les caractères ayant une signification
spéciale ("
et &
) par des entités HTML.
Attention, le bon ordre est important.
Comme vous pouvez le voir, le même littéral JavaScript peut être encodé
différemment dans l'élément <script>
et différemment dans
l'attribut !
Question n° 7
Revenons de JavaScript à HTML. Quels caractères devons-nous remplacer à l'intérieur du commentaire et de quelle manière ?
<!-- <?= $str ?> -->
Solution : à l'intérieur d'un commentaire HTML (et XML), tous les
caractères spéciaux traditionnels, tels que <
,
&
, "
et '
, peuvent apparaître. Ce
qui est interdit, et cela vous surprendra probablement, c'est la paire de
caractères --
. L'échappement de cette séquence n'est pas
spécifié, c'est donc à vous de décider comment la remplacer. Vous pouvez les
espacer. Ou peut-être les remplacer par ==
.
Question n° 8
Nous approchons de la fin, alors essayons de varier la question. Essayez de réfléchir à ce à quoi il faut faire attention lors de l'affichage de la variable dans ce contexte :
<a href="<?= $str ?>">...</a>
Solution : outre l'échappement, il est important de vérifier également que
l'URL ne contient pas de schéma dangereux comme javascript:
, car
une URL ainsi construite appellerait le code de l'attaquant après un clic.
Question n° 9
Pour finir, une perle pour les vrais connaisseurs. Il s'agit d'un exemple
d'application utilisant un framework JavaScript moderne, en l'occurrence Vue.
Voyons si vous pensez à ce à quoi il faut faire attention lors de l'affichage
de la variable à l'intérieur de l'élément #app
:
<div id="app">
<?= $str ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
...
})
</script>
Ce code crée une application Vue qui sera rendue dans l'élément
#app
. Vue comprend le contenu de cet élément comme son template.
Et à l'intérieur du template, il interprète
les doubles accolades, qui signifient l'affichage d'une variable ou l'appel de
code javascript (par exemple {{ foo }}
).
Donc, à l'intérieur de l'élément #app
, outre les caractères
<
et &
, la paire {{
a également
une signification spéciale, que nous devons remplacer par une autre séquence
correspondante pour que Vue ne l'interprète pas comme sa balise. Le
remplacement par des entités HTML n'aide cependant pas dans ce cas. Comment
s'en sortir ? Une astuce fonctionne : nous insérons un commentaire HTML vide
entre les accolades {<!-- -->{
et Vue ignore une telle
séquence.
Résultats du quiz
Comment vous en êtes-vous sorti dans le quiz ? Combien de bonnes réponses avez-vous ? Si vous avez répondu correctement à au moins 4 questions, vous faites partie des 8 % des meilleurs participants – félicitations !
Cependant, pour assurer la sécurité de votre site web, il est essentiel de traiter correctement la sortie dans toutes les situations.
Si vous avez été surpris par le nombre de contextes différents qui peuvent apparaître sur une page HTML ordinaire, sachez que nous n'avons pas mentionné tous les contextes, loin de là. Ce serait un quiz beaucoup plus long. Néanmoins, vous n'avez pas besoin d'être un expert en échappement dans chaque contexte si votre système de template peut le gérer.
Essayons-les donc.
Comment se comportent les systèmes de template ?
Tous les systèmes de template modernes se vantent d'une fonction d'auto-échappement, qui échappe automatiquement toutes les variables affichées. S'ils le font correctement, votre site est en sécurité. S'ils le font mal, le site est exposé au risque de vulnérabilité XSS avec toutes ses conséquences graves.
Nous allons tester les systèmes de template populaires à partir des questions de ce quiz pour voir à quel point leur auto-échappement est efficace. Le test comparatif des systèmes de template pour PHP commence.
Twig ❌
Le premier en lice est le système de template Twig (version 3.5), qui est le plus souvent
utilisé en conjonction avec le framework Symfony. Nous allons lui confier la
tâche de répondre à toutes les questions du quiz. La variable
$str
sera toujours remplie d'une chaîne piégeuse et nous verrons
comment il gère son affichage. Les résultats sont visibles à droite. Vous
pouvez également explorer
ses réponses et son comportement sur le terrain de jeu.
{% set str = "<'\"&" %}
1) <p>{{ str }}</p>
2) <input value="{{ str }}">
3) <input value='{{ str }}'>
{% set str = "foo onclick=evilCode()" %}
4) <input value={{ str }}>
{% set str = "'\"\n\u{2028}" %}
5) <script> let foo = '{{ str }}'; </script>
6) <p onclick="foo('{{ str }}')"></p>
{% set str = "-- ---" %}
7) <!-- {{ str }} -->
{% set str = "javascript:evilCode()" %}
8) <a href="{{ str }}">...</a>
{% set str = "{{ foo }}" %}
9) <div id="app"> {{ str }} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"u{2028}; </script>
❌ <p onclick="foo('"u{2028})"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
Twig a échoué dans six des neuf tests !
Malheureusement, l'échappement automatique de Twig ne fonctionne que dans le texte HTML et les attributs, et de plus, uniquement s'ils sont entourés de guillemets. Dès que les guillemets manquent, Twig ne signale aucune erreur et crée une faille de sécurité XSS.
C'est particulièrement gênant, car c'est ainsi que les valeurs des attributs sont écrites dans les bibliothèques populaires comme React ou Svelte. Un programmeur qui utilise à la fois Twig et React peut donc tout naturellement oublier les guillemets.
L'auto-échappement de Twig échoue également dans tous les autres exemples.
Dans les contextes (5) et (6), il faut échapper manuellement à l'aide de
{{ str|escape('js') }}
, pour les autres contextes, Twig ne propose
même pas de fonction d'échappement. Il ne dispose pas non plus de protection
contre l'affichage d'un lien malveillant (8) ou de prise en charge des templates
pour Vue (9).
Blade ❌❌
Le deuxième participant est le système de template Blade (version 10.9), qui est étroitement intégré à Laravel et à son écosystème. Nous vérifierons à nouveau ses capacités sur nos questions de quiz. Vous pouvez également explorer ses réponses sur le terrain de jeu.
@php($str = "<'\"&")
1) <p>{{ $str }}</p>
2) <input value="{{ $str }}">
3) <input value='{{ $str }}'>
@php($str = "foo onclick=evilCode()")
4) <input value={{ $str }}>
@php($str = "'\"\n\u{2028}")
5) <script> let foo = {{ $str }}; </script>
6) <p onclick="foo({{ $str }})"></p>
@php($str = "-- ---")
7) <!-- {{ $str }} -->
@php($str = "javascript:evilCode()")
8) <a href="{{ $str }}">...</a>
@php($str = "{{ foo }}")
9) <div id="app"> {{ $str }} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '" ; </script>
❌ <p onclick="foo('" )"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌❌ <div id="app"> <?php echo e(foo); ?> </div>
Blade a échoué dans six des neuf tests !
Le résultat est similaire à celui de Twig. Encore une fois, l'échappement
automatique ne fonctionne que dans le texte HTML et les attributs, et uniquement
s'ils sont entourés de guillemets. L'auto-échappement de Blade échoue
également dans tous les autres exemples. Dans les contextes (5) et (6), il est
nécessaire d'échapper manuellement à l'aide de
{{ Js::from($str) }}
. Pour les autres contextes, Blade ne propose
même pas de fonction d'échappement. Il ne dispose pas de protection contre
l'affichage d'un lien malveillant (8) ni de prise en charge des templates pour
Vue (9).
Ce qui est cependant surprenant, c'est l'échec de la directive
@php
dans Blade, ce qui provoque l'affichage du propre code PHP
directement dans la sortie, comme vous pouvez le voir sur la
dernière ligne.
Smarty ❌❌❌
Nous testons maintenant le plus ancien système de template pour PHP, qui est
Smarty (version 4.3). À la grande
surprise, ce système n'a pas d'auto-échappement actif. Vous devez donc soit
indiquer le filtre {$var|escape}
à chaque fois que vous affichez
des variables, soit activer l'auto-échappement HTML. L'information à ce sujet
est assez enfouie dans la documentation.
{$str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>
{$str = "foo onclick=evilCode()"}
4) <input value={$str}>
{$str = "'\"\n\u{2028}"}
5) <script> let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>
{$str = "-- ---"}
7) <!-- {$str} -->
{$str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>
{$str = "{{ foo }}"}
9) <div id="app"> {$str} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
❌ <input value=foo onclick=evilCode()>
❌ <script> let foo = '"\u2028; </script>
❌ <p onclick="foo('"\u2028)"></p>
❌ <!-- -- --- -->
❌ <a href="javascript:evilCode()">...</a>
❌ <div id="app"> {{ foo }} </div>
Smarty a échoué dans six des neuf tests !
Le résultat est à première vue similaire à celui des bibliothèques
précédentes. Smarty ne peut échapper automatiquement que dans le texte HTML
et les attributs, et seulement lorsque les valeurs sont entourées de
guillemets. Partout ailleurs, il échoue. Dans les contextes (5) et (6), il faut
échapper manuellement à l'aide de {$str|escape:javascript}
. Mais
cela n'est possible que si l'auto-échappement HTML n'est pas actif, sinon ces
échappements entrent en conflit. Smarty est donc, du point de vue de la
sécurité, le fiasco total de ce test.
Latte ✅
Le trio se termine par le système de template Latte (version 3.0). Nous allons tester son auto-échappement. Vous pouvez également explorer ses réponses et son comportement sur le terrain de jeu.
{var $str = "<'\"&"}
1) <p>{$str}</p>
2) <input value="{$str}">
3) <input value='{$str}'>
{var $str = "foo onclick=evilCode()"}
4) <input value={$str}>
{var $str = "'\"\n\u{2028}"}
5) <script> let foo = {$str}; </script>
6) <p onclick="foo({$str})"></p>
{var $str = "-- ---"}
7) <!-- {$str} -->
{var $str = "javascript:evilCode()"}
8) <a href="{$str}">...</a>
{var $str = "{{ foo }}"}
9) <div id="app"> {$str} </div>
✅ <p><'"&</p>
✅ <input value="<'"&">
✅ <input value='<'"&'>
✅ <input value="foo onclick=evilCode()">
✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo("'\"\n\u2028")"></p>
✅ <!-- - - - - - -->
✅ <a href="">...</a>
✅ <div id="app"> {<!-- -->{ foo }} </div>
Latte a excellé dans les neuf tâches !
Il a réussi à gérer les guillemets manquants pour les attributs HTML, a
réussi à traiter JavaScript à la fois dans l'élément
<script>
et dans les attributs, et a réussi à gérer même
la séquence interdite dans les commentaires HTML.
De plus, il a évité la situation où un clic sur un lien falsifié par un attaquant pourrait exécuter son code. Et il a réussi à gérer l'échappement des balises pour Vue.
Test bonus
L'une des capacités essentielles de tous les systèmes de template est de
travailler avec des blocs et l'héritage de templates qui y est associé. Nous
allons donc confier une tâche supplémentaire à tous les systèmes de template
testés. Nous allons créer un bloc description
que nous
afficherons dans un attribut HTML. Dans le monde réel, bien sûr, la
définition du bloc se trouverait dans un template enfant et son affichage dans
un template parent, par exemple un layout. Ceci n'est qu'une forme simplifiée,
mais suffisante pour tester l'auto-échappement lors de l'affichage des blocs.
Comment s'en sont-ils sortis ?
Twig : échec ❌ lors de l'affichage des blocs, les caractères ne sont pas traités
{% block description %}
rock n' roll
{% endblock %}
<meta name='description'
content='{{ block('description') }}'>
<meta name='description'
content=' rock n' roll '> ❌
Blade : échec ❌ lors de l'affichage des blocs, les caractères ne sont pas traités
@section('description')
rock n' roll
@endsection
<meta name='description'
content='@yield('description')'>
<meta name='description'
content=' rock n' roll '> ❌
Latte : réussi ✅ lors de l'affichage des blocs, a correctement traité les caractères problématiques
{block description}
rock n' roll
{/block}
<meta name='description'
content='{include description}'>
<meta name='description'
content=' rock n' roll '> ✅
Pourquoi tant de sites web sont-ils vulnérables ?
L'auto-échappement dans des systèmes comme Twig, Blade ou Smarty fonctionne
en remplaçant simplement cinq caractères <>"'&
par des
entités HTML et ne distingue en aucun cas le contexte. C'est pourquoi il ne
fonctionne que dans certaines situations et échoue dans toutes les autres.
L'auto-échappement naïf est une fonction dangereuse, car il crée un faux
sentiment de sécurité.
Il n'est donc pas surprenant qu'actuellement plus de 27 % des sites web présentent des vulnérabilités critiques, principalement XSS (source : Acunetix Web Vulnerability Report). Comment s'en sortir ? Utiliser un système de template qui distingue les contextes.
Latte est le seul système de template en PHP qui ne perçoit pas un template comme une simple chaîne de caractères, mais comprend le HTML. Il comprend ce que sont les balises, les attributs, etc. Il distingue les contextes. Et c'est pourquoi il échappe correctement dans le texte HTML, différemment à l'intérieur d'une balise HTML, différemment à l'intérieur de JavaScript, etc.
Latte représente ainsi le seul système de template sécurisé.
De plus, grâce à sa compréhension du langage HTML, il offre les merveilleux n:attributs, que les utilisateurs adorent:
<ul n:if="$menu">
<li n:foreach="$menu->getItems() as $item">{$item->title}</li>
</ul>
Pour soumettre un commentaire, veuillez vous connecter