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

il y a 10 mois de David Grudl  

Testez vos connaissances et vos compétences en matière de sécurité dans ce quiz ! Pouvez-vous empêcher un attaquant de prendre le contrôle d'une page HTML ?

Dans toutes les tâches, vous aborderez la même question : comment afficher correctement la variable $str dans une page HTML sans créer de vulnérabilité XSS. La base de la défense est le escaping, qui consiste à remplacer les caractères ayant une signification spéciale par des séquences correspondantes. Par exemple, lors de la sortie d'une chaîne de caractères en texte HTML, dans laquelle le caractère < a une signification particulière (il indique le début d'une balise), nous le remplaçons par l'entité HTML &lt;, et le navigateur affiche correctement le symbole <.

Soyez vigilant, car la vulnérabilité XSS est très grave. Elle peut permettre à un attaquant de prendre le contrôle d'une page ou même du compte d'un utilisateur. Bonne chance et réussissez à sécuriser la page HTML !

Le premier trio de questions

Précisez quels caractères doivent être traités et comment dans les premier, deuxième et troisième exemples :

1) <p><?= $str ?></p>
2) <input value="<?= $str ?>">
3) <input value='<?= $str ?>'>

Si la sortie n'était traitée d'aucune manière, elle ferait partie de la page affichée. Si un attaquant parvient à insérer la chaîne 'foo" onclick="evilCode()' dans une variable et que la sortie n'est pas traitée, son code sera exécuté lorsqu'il cliquera sur l'élément :

$str = 'foo" onclick="evilCode()'
❌ not treated: <input value="foo" onclick="evilCode()">
✅ treated:     <input value="foo&quot; onclick=&quot;evilCode()">

Solutions pour chaque exemple :

  1. les caractères < et & représentent le début d'une balise et d'une entité HTML ; remplacez-les par &lt; et . &amp;
  2. les caractères " et & représentent la fin d'une valeur d'attribut et le début d'une entité HTML ; remplacez-les par &quot; et &amp;
  3. les caractères ' et & représentent la fin d'une valeur d'attribut et le début d'une entité HTML ; remplacez-les par &apos; et &amp;

Vous obtenez un point pour chaque bonne réponse. Bien entendu, dans les trois cas, vous pouvez également remplacer d'autres caractères par des entités ; cela ne pose aucun problème, mais ce n'est pas nécessaire.

Question n° 4

Quels sont les caractères qui doivent être remplacés lors de l'affichage d'une variable dans ce contexte ?

<input value=<?= $str ?>>

La solution : Comme vous pouvez le constater, les guillemets manquent ici. La solution la plus simple est d'ajouter les guillemets et d'y échapper comme dans la question précédente. Il existe également une deuxième solution, qui consiste à remplacer les espaces et tous les caractères ayant une signification particulière à l'intérieur d'une balise, tels que >, /, =, et quelques autres par des entités HTML.

Question n° 5

La situation devient de plus en plus intéressante. Quels sont les personnages qui doivent être traités 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 il existe une règle spéciale. Quels sont donc les caractères à échapper ? À l'intérieur de la chaîne JavaScript, nous échappons naturellement au caractère ' qui la délimite, en utilisant une barre oblique inverse et en le remplaçant par \'. Étant donné que JavaScript ne prend pas en charge les chaînes multi-lignes (sauf en tant que template literals), nous devons également échapper aux caractères de nouvelle ligne. Cependant, il faut savoir qu'en plus des caractères habituels \n et \r, JavaScript considère également les caractères Unicode \u2028 et \u2029 comme des caractères de retour à la ligne, auxquels nous devons également échapper. Enfin, la règle spéciale mentionnée : la chaîne ne doit pas contenir </script. On peut l'éviter, par exemple, en le remplaçant par <\/script.

Si vous le saviez, félicitations.

Question n° 6

Le contexte suivant semble n'être qu'une variante du précédent. Pensez-vous que le traitement sera différent ?

<p onclick="foo('<?= $str ?>')"></p>

Solution : Ici encore, les règles d'échappement des chaînes JavaScript s'appliquent, mais contrairement au contexte précédent où les entités HTML n'étaient pas échappées, elles le sont ici. Nous commençons donc par échapper la chaîne JavaScript à l'aide de barres obliques inverses, puis nous remplaçons les caractères spéciaux (" et &) par des entités HTML. Attention, l'ordre est important.

Comme vous pouvez le constater, le même littéral JavaScript peut être encodé différemment dans un élément <script> et différemment dans un attribut !

Question n° 7

Passons de JavaScript à HTML. Quels caractères devons-nous remplacer à l'intérieur du commentaire et comment ?

<!-- <?= $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 peut vous surprendre, c'est la paire de caractères --. L'échappement de cette séquence n'est pas spécifié, il vous appartient donc de la remplacer. Vous pouvez les intercaler avec des espaces. Ou, par exemple, les remplacer par ==.

Question n° 8

Nous approchons de la fin, alors essayons de varier la question. Essayez de réfléchir à ce à quoi vous devez faire attention lorsque vous imprimez une variable dans ce contexte :

<a href="<?= $str ?>">...</a>

Solution : En plus de l'échappement, il est important de vérifier que l'URL ne contient pas de schéma dangereux comme javascript:, car une URL composée de la sorte exécuterait le code de l'attaquant lorsqu'elle serait cliquée.

Question n° 9

Enfin, un régal pour les vrais connaisseurs. Il s'agit d'un exemple d'application utilisant un cadre JavaScript moderne, en l'occurrence Vue. Voyons si vous pouvez trouver ce à quoi il faut faire attention lors de l'impression d'une 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 interprète le contenu de cet élément comme son modèle. Et à l'intérieur du modèle, il interprète les doubles accolades, qui représentent la sortie d'une variable ou l'appel d'un code JavaScript (par exemple…), {{ foo }}).

Ainsi, dans l'élément #app, outre les caractères < et &, la paire {{ a également une signification particulière, que nous devons remplacer par une autre séquence appropriée pour éviter que Vue ne l'interprète comme sa propre balise. Le remplacement par des entités HTML n'est pas utile dans ce cas. Comment faire ? Il existe une astuce : insérez un commentaire HTML vide entre les accolades {<!-- -->{et Vue ignore cette séquence.

Résultats du quiz

Quels sont vos résultats au quiz ? Combien de réponses correctes avez-vous ? Si vous avez répondu correctement à au moins 4 questions, vous faites partie des 8 % de personnes ayant le mieux résolu les problèmes – félicitations !

Toutefois, pour assurer la sécurité de votre site web, vous devez traiter correctement les données de 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 typique, sachez que nous ne les avons pas tous mentionnés. Cela rendrait le quiz beaucoup plus long. Néanmoins, vous n'avez pas besoin d'être un expert en matière d'échappement dans tous les contextes si votre système de templates peut le gérer.

Alors, testons-les.

Quelles sont les performances des systèmes de templates ?

Tous les systèmes de création de modèles modernes sont dotés d'une fonction autoescaping qui échappe automatiquement toutes les variables produites. S'ils le font correctement, votre site web 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 des systèmes de templates populaires à partir des questions de ce quiz afin de déterminer l'efficacité de leur échappement automatique. Commençons l'examen des systèmes de templates PHP.

Twig ❌

Tout d'abord, le système de templating Twig (version 3.5), le plus souvent utilisé en conjonction avec le framework Symfony. Nous allons le charger de répondre à toutes les questions du quiz. La variable $str sera toujours remplie avec une chaîne de caractères délicate, et nous verrons comment il gère sa sortie. Vous pouvez voir les résultats sur la 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é à six tests sur neuf!

Malheureusement, l'échappement automatique de Twig ne fonctionne que pour le texte et les attributs HTML, et encore, uniquement lorsqu'ils sont entre guillemets. Dès que les guillemets sont absents, Twig ne signale aucune erreur et crée une faille de sécurité XSS.

C'est particulièrement désagréable car c'est ainsi que les valeurs d'attributs sont écrites dans des bibliothèques populaires comme React ou Svelte. Un programmeur qui utilise à la fois Twig et React peut tout naturellement oublier les guillemets.

L'auto-capsulage de Twig échoue également dans tous les autres exemples. Dans les contextes (5) et (6), un échappement manuel est nécessaire en utilisant {{ str|escape('js') }}tandis que pour les autres contextes, Twig ne propose même pas de fonction d'échappement. Il manque également une protection contre l'impression d'un lien malveillant (8) ou un support pour les modèles Vue (9).

Blade ❌❌

Le deuxième participant est le système de templating Blade (version 10.9), qui est étroitement intégré à Laravel et à son écosystème. Là encore, nous allons tester 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>

La lame a échoué dans six tests sur neuf !

Le résultat est similaire à celui de Twig. Là encore, l'échappement automatique ne fonctionne que pour le texte et les attributs HTML, et uniquement s'ils sont placés entre guillemets. L'échappement automatique de Blade échoue également dans tous les autres exemples. Dans les contextes (5) et (6), un échappement manuel est nécessaire en utilisant {{ Js::from($str) }}. Pour les autres contextes, Blade ne propose même pas de fonction d'échappement. Il manque également une protection contre l'impression d'un lien malveillant (8) ou un support pour les modèles Vue (9).

Cependant, ce qui est surprenant, c'est l'échec de la directive @php dans Blade, qui provoque la sortie de son propre code PHP directement dans la sortie, comme on peut le voir dans la dernière ligne.

Latte ✅

Le trio est conclu par le système de templating Latte (version 3.0). Nous allons tester son autoescaping. 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 est parvenu à gérer les guillemets manquants dans les attributs HTML, à traiter le JavaScript à la fois dans l'élément <script> et dans les attributs, et a traité la séquence interdite dans les commentaires HTML.

De plus, il a empêché qu'en cliquant sur un lien malveillant fourni par un attaquant, le code de ce dernier puisse être exécuté. Enfin, 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 templates est de travailler avec des blocs et l'héritage de templates correspondant. C'est pourquoi nous allons donner une tâche supplémentaire à tous les systèmes de création de modèles testés. Nous allons créer un bloc description, que nous imprimerons dans un attribut HTML. Dans le monde réel, la définition du bloc serait, bien entendu, située dans le modèle enfant et sa sortie dans le modèle parent, tel que la mise en page. Ce n'est qu'une forme simplifiée, mais elle est suffisante pour tester l'auto-capsule lors de la sortie des blocs. Comment se sont-elles comportées ?

Twig : failed ❌ lors de la sortie de blocs, les caractères ne sont pas correctement échappés

{% block description %}
	rock n' roll
{% endblock %}

<meta name='description'
	content='{{ block('description') }}'>




<meta name='description'
	content=' rock n' roll '> ❌

Lame : échec ❌ lors de la sortie de blocs, les caractères ne sont pas correctement échappés

@section('description')
	rock n' roll
@endsection

<meta name='description'
	content='@yield('description')'>




<meta name='description'
	content=' rock n' roll '> ❌

Latte : passé ✅ lors de la sortie des blocs, il gère correctement 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-capsulage dans des systèmes tels que Twig, Blade ou Smarty remplace simplement les cinq caractères <>"'& par des entités HTML et ne tient pas compte du contexte. Par conséquent, il ne fonctionne que dans certaines situations et échoue dans toutes les autres. L'auto-scaping naïf est une fonctionnalité dangereuse parce qu'elle crée un faux sentiment de sécurité.

Il n'est donc pas surprenant qu'à l'heure actuelle, plus de 27 % des sites web présentent des vulnérabilités critiques, principalement des XSS (source : Acunetix Web Vulnerability Report). Comment sortir de cette situation ? Utiliser un système de templating qui distingue les contextes.

Latte est le seul système de templates PHP qui ne perçoit pas un template comme une simple chaîne de caractères, mais qui comprend le HTML. Il sait ce que sont les balises, les attributs, etc. Il distingue les contextes. Et donc, il échappe correctement dans le texte HTML, différemment à l'intérieur des balises HTML, différemment à l'intérieur de JavaScript, etc.

Latte représente donc le seul système de création de modèles 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>