Quiz : pouvez-vous vous défendre contre la vulnérabilité XSS ?

il y a 2 ans par David Grudl  

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 &lt; 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&quot; onclick=&quot;evilCode()">

Solution des exemples individuels :

  1. les caractères < et & représentent le début d'une balise HTML et d'une entité, nous les remplaçons par &lt; et &amp;
  2. 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 &quot; et &amp;
  3. 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 &apos; et &amp;

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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;u{2028}; </script>
❌ <p onclick="foo(&#039;&quot;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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script>	let foo = &#039;&quot; ; </script>
❌ <p onclick="foo(&#039;&quot; )"></p>


❌ <!-- -- --- -->


❌ <a href="javascript:evilCode()">...</a>


❌❌ <div id="app"> &lt;?php echo e(foo); ?&gt; </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>&lt;&#039;&quot;&amp;</p>
✅ <input value="&lt;&#039;&quot;&amp;">
✅ <input value='&lt;&#039;&quot;&amp;'>


❌ <input value=foo onclick=evilCode()>


❌ <script> let foo = &#039;&quot;\u2028; </script>
❌ <p onclick="foo(&#039;&quot;\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>&lt;'"&amp;</p>
✅ <input value="&lt;&apos;&quot;&amp;">
✅ <input value='&lt;&apos;&quot;&amp;'>


✅ <input value="foo onclick=evilCode()">


✅ <script> let foo = "'\"\n\u2028"; </script>
✅ <p onclick="foo(&quot;&apos;\&quot;\n\u2028&quot;)"></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&apos; 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>