Examen de spécialiste certifié en sécurité de l'information, automne Reiwa 5, question 1 de l'après-midi — le XSS stocké qui réduit 16 avis à 2

· · Spécialiste certifié en sécurité de l'information, RISS, XSS, Cross-site scripting, Application web, Sécurité de l'information, Vulnérabilité, IPA, Gestion de session

« Il devrait y avoir 16 avis, mais seuls 2 s’affichent. »

La question 1 de l’après-midi de l’examen de spécialiste certifié en sécurité de l’information, session d’automne Reiwa 5, commence par cette remontée d’un utilisateur1. Rien d’anormal dans la navigation, aucune erreur ne s’affiche. Seulement, le nombre affiché ne correspond pas.

La cause était un cross-site scripting (XSS) de type stocké. Mais ce qui rend cette question intéressante n’est pas la réponse « XSS » en elle-même : c’est le fait que toutes les « contre-mesures plausibles » que la société Q (le commerçant en ligne mis en scène dans l’énoncé) avait déjà mises en place ont été contournées, l’une après l’autre. La limite de 50 caractères du titre d’avis a été franchie, le jeton nécessaire au téléversement a été obtenu par une procédure parfaitement régulière, et l’identifiant de session volé a été exfiltré sans jamais être envoyé vers un serveur externe.

Cet article suit cette grande question sous-question par sous-question, pour clarifier où se situe la frontière entre les contre-mesures qui n’ont pas fonctionné et celles qui auraient dû fonctionner.

Ce que cet article apporte, au-delà de la méthode de résolution de l’examen (les réponses types de chaque sous-question et leur justification), c’est une vue d’ensemble des contre-mesures XSS utilisables en pratique professionnelle. Il est écrit de façon à rester cohérent que vous lisiez les sections dans l’ordre des sous-questions en préparation à l’examen, ou que vous ne lisiez d’emblée que les chapitres 9 et 10 pour la seule perspective de la revue de code en entreprise.

Par ailleurs, la question plus générale de savoir sur quels critères vérifier la sécurité d’un site web dans son ensemble est traitée dans « Utiliser « Comment concevoir un site web sûr » de l’IPA comme liste de contrôle ». Cet article approfondit, à travers un cas concret, l’une des 11 vulnérabilités qui y sont recensées : le XSS.

1. Pour commencer, la conclusion

  • Le type de vulnérabilité est un XSS stocké. La chaîne de l’attaquant a été enregistrée sur le serveur, puis renvoyée dans le HTML de toute personne ouvrant la page par la suite. Le fait que le script injecté utilise des API du DOM est une chose distincte du fait qu’il s’agisse d’un DOM Based XSS.
  • La limite de caractères en entrée n’a pas fait office de contre-mesure XSS. L’attaquant a fractionné sa publication en 15 fois, en faisant sauter, via des commentaires JavaScript, le HTML intercalé entre les publications, pour relier le tout en un seul script.
  • Le jeton de téléversement (contre-mesure CSRF) n’a pas non plus fait office de contre-mesure XSS. Le script d’attaque obtient le jeton par la même procédure qu’un écran légitime. Dès lors qu’il s’exécute sur la même origine, il peut faire tout ce qu’un utilisateur légitime peut faire.
  • L’identifiant de session volé n’a jamais été envoyé vers l’extérieur. Le contenu du cookie a été placé, sous forme de fichier image « a.png », dans la fonction de téléversement d’icône du site lui-même, et l’attaquant l’a récupéré simplement en le consultant normalement. Les contre-mesures de type « sortie » ne peuvent pas détecter cela.
  • L’énoncé désigne explicitement trois éléments manquants chez la société Q : l’échappement à la sortie (solution fondamentale), ainsi que l’attribut HttpOnly du cookie et la vérification du format du fichier téléversé (ces deux derniers étant des contre-mesures de type assurance). N’importe lequel de ces trois éléments, à lui seul, aurait suffi à briser la chaîne de cette attaque. La vérification du format, toutefois, peut être contournée si l’attaquant change de méthode, ce qui impose d’aller jusqu’au réencodage.
  • Bien que l’énoncé n’en parle pas, la directive script-src de la CSP peut elle aussi briser la même chaîne. Avec une configuration qui n’autorise pas 'unsafe-inline', le script en ligne injecté ne serait de toute façon jamais exécuté.

2. Le sujet — la source et la façon dont cet article le traite

Voici la question retenue.

Source : Examen de spécialiste certifié en sécurité de l’information, session d’automne de l’année Reiwa 5 (2023), après-midi, question 1

L’IPA indique que, sauf disposition légale particulière, aucune autorisation ni redevance n’est nécessaire pour l’utilisation des questions d’examens passés qu’elle publie. Cela ne signifie toutefois pas qu’elle renonce à ses droits d’auteur : elle demande que la source soit indiquée sous la forme « année, période, catégorie d’examen, tranche horaire, numéro de question, etc. », et que toute modification apportée à l’énoncé soit également signalée.2

Cet article ne reproduit pas tel quel le HTML ni les scripts publiés dans le livret d’examen. Dans la mesure nécessaire pour expliquer le mécanisme, ils sont remplacés par du code d’exemple équivalent que nous avons rédigé nous-mêmes. L’énoncé des sous-questions et les réponses types sont également traités sous forme résumée. Le livret d’examen original, les réponses types et le commentaire de correction peuvent être téléchargés gratuitement depuis le site de l’IPA ; nous vous recommandons de les garder ouverts pendant la lecture.1 3 4

Correspondance entre les sous-questions et cet article

Pour celles et ceux qui lisent en gardant le livret d’examen ouvert, voici la correspondance entre les sous-questions et les sections de cet article. Vous pouvez tout à fait commencer votre lecture directement à la sous-question qui vous intéresse.

Sous-question Ce qui est demandé (nombre de caractères) Section correspondante de cet article
Sous-question 1(1) Le type de vulnérabilité XSS exploitée (choix parmi 3) Chapitre 4
Sous-question 1(2) La contre-mesure dans l’application web Q (30 caractères max) Chapitre 4, « Sous-question 1(2) : la contre-mesure »
Sous-question 2 La méthode utilisée pour exécuter un script dépassant la limite de caractères en entrée (50 caractères max) Chapitre 5
Sous-question 3(1) Le traitement effectué par les lignes 6 à 20 du script d’attaque (60 caractères max) Chapitre 6
Sous-question 3(2) La méthode par laquelle l’attaquant récupère les informations téléversées (50 caractères max) Chapitre 7
Sous-question 3(3) Ce que permettent de faire les informations récupérées (40 caractères max) Chapitre 7, « Sous-question 3(3) : ce que permet l’identifiant de session »
Sous-question 4 Le mécanisme du navigateur qui empêche l’attaque de réussir depuis le domaine de l’attaquant (40 caractères max) Chapitre 8

Si vous n’avez besoin que de la partie pratique professionnelle, sans passer par les sous-questions, commencez par les chapitres 9 (liste des contre-mesures efficaces et inefficaces) et 10 (points de revue de code). Pour voir la chaîne d’attaque en un seul coup d’œil, le diagramme de séquence du chapitre 7 donne la vue d’ensemble, de la publication jusqu’à la récupération.

Correspondance entre le contenu du livret d’examen et les exemples de cet article

Pour permettre la comparaison avec le texte original, voici un résumé de ce qui a été remplacé et comment.

Contenu du livret d’examen Traitement dans cet article Emplacement
HTML de la page V (contenant les titres d’avis publiés en plusieurs fois) Non reproduit tel quel : exemple équivalent rédigé par nos soins, réduit à 3 publications tout en conservant le même mécanisme Chapitre 5
Script d’attaque extrait (environ 20 lignes) Non reproduit tel quel : JavaScript équivalent effectuant le même traitement, rédigé par nos soins. Les commentaires dans le code sont ajoutés pour les besoins de l’explication de cet article Chapitre 6
Énoncé de chaque sous-question Résumé conservant le sens (les contraintes comme la limite de caractères reprennent les valeurs du texte original) Début de chaque chapitre 4 à 8
Réponses types Réponses types publiées par l’IPA3 Divers endroits des chapitres 4 à 8
Commentaire de correction Extraits pertinents du commentaire de correction publié par l’IPA4 Chapitres 4, 5, 7
Spécifications de l’énoncé (société Q, page V, membres A/B, fonctions et limites de caractères, etc.) Résumé fidèle au texte original Chapitre 2, « Le contexte du problème »

Le contexte du problème

L’énoncé met en scène la société Q, un commerçant en ligne de vêtements comptant 100 employés. Elle exploite son site e-commerce avec « l’application web Q », développée en interne, à laquelle les utilisateurs accèdent en HTTPS. Le scénario est qu’elle vient d’ajouter une fonction d’avis produit réservée aux membres.

Voici les cinq points de spécification à retenir.

Fonction Spécification
Connexion Authentifie avec l’identifiant de membre et le mot de passe, et délivre un identifiant de session sous forme de cookie
Avis produit Seuls les membres connectés peuvent publier. Le titre de l’avis est limité à 50 caractères, le détail de l’avis à 300 caractères, tous deux en texte libre
Profil membre Propose une page de téléversement d’icône et une page d’enregistrement des informations de carte bancaire. Les deux ne sont accessibles qu’aux membres connectés
Téléversement de l’icône Envoie un fichier image et un jeton en paramètres vers /user/upload. Ne réussit que si le jeton correspond à celui délivré par /user/profile
Affichage de l’icône L’icône téléversée s’affiche sur la page de paramètres du profil membre et sur les pages d’avis

Les deux dernières lignes joueront un rôle plus tard.

3. Le symptôme — pourquoi 16 avis sont devenus 2

Un membre signale que « sur la page d’avis du T-shirt uni (page V), 16 avis devraient s’afficher mais seuls 2 apparaissent ». Lorsque M. N, du service développement, ouvre la page V, voici ce qu’il voit à l’écran.

  • L’en-tête affiche « 16 avis »
  • Un avis du membre A (titre « Good », texte « Nice shirt! »)
  • Un avis du membre B
  • En bas, « Fin des 16 avis »

Le compteur affiche 16, mais seuls 2 avis apparaissent réellement. En examinant le HTML, on découvre que le membre A a en réalité publié 15 avis, et qu’un long script y est intégré.

Autrement dit, les 16 avis sont bien tous présents dans le HTML généré. S’il n’en apparaît que 2 à l’écran, c’est parce que la majeure partie s’est retrouvée à l’intérieur d’un élément <script>, que le navigateur ne traite plus comme un « contenu à afficher ».

La portion « avalée » n’est pas, précisément, « les 15 avis en entier ». La balise ouvrante <script> apparaît au milieu du titre du premier avis (juste après « Good »), et la balise fermante </script> se trouve à la fin du titre du quinzième avis. Par conséquent :

  • la carte du premier avis (icône, nom affiché, date, étoiles, jusqu’au « Good » du titre) précède le <script> et s’affiche donc
  • de là jusqu’à la fin du titre du quinzième avis, tout est avalé comme contenu de l’élément script
  • le reste du quinzième avis (le texte « Nice shirt! ») suit le </script> et s’affiche donc

Résultat : l’en-tête du premier avis se raccorde au texte du quinzième, et l’écran donne l’impression d’« un seul avis du membre A, avec pour titre Good et pour texte Nice shirt! ». En y ajoutant l’avis du membre B, cela fait 2. Si l’attaquant a placé « Good » au début du premier avis et « Nice shirt! » dans le texte du quinzième — des chaînes qui paraissent naturelles — c’est sans doute pour que la casse d’affichage ne paraisse pas suspecte.

Savoir interpréter ce symptôme — « le compte est bon mais l’affichage ne correspond pas » — est aussi utile en pratique professionnelle. C’est une porte d’entrée pour soupçonner, non pas un bug dans la logique de comptage, mais la possibilité que la structure du HTML généré ait été corrompue.

4. Pourquoi un XSS « stocké » — sous-question 1

La sous-question 1 demande de choisir, parmi trois options — DOM Based XSS, XSS stocké, XSS réfléchi — le type de vulnérabilité XSS utilisé dans cette attaque. La bonne réponse est XSS stocké.

Et à propos de cette sous-question, le commentaire de correction de l’IPA indique ceci :

Le taux de bonnes réponses était dans la moyenne, mais on a observé un certain nombre de candidats répondant à tort « DOM Based XSS », sans doute parce que le script utilisait le DOM.

Le script d’attaque figurant dans l’énoncé utilise XMLHttpRequest, reçoit la réponse sous forme de DOM et récupère un élément avec getElementById. Il manipule bien le DOM, c’est vrai. Mais cela n’a aucun rapport avec le type de vulnérabilité.

La frontière se situe dans « où la chaîne d’attaque devient-elle un script »

Ce qui distingue les trois types, c’est l’endroit où la chaîne d’attaque se transforme en code exécutable — autrement dit, si le point de sortie vulnérable (le sink) se trouve côté serveur ou côté navigateur.

Type Sink (endroit où la chaîne d’attaque devient exécutable) D’où vient la chaîne d’attaque
XSS réfléchi Le traitement qui construit le HTML côté serveur La requête elle-même, par exemple un paramètre d’URL préparé par l’attaquant
XSS stocké Le traitement qui construit le HTML côté serveur Des données enregistrées côté serveur
DOM Based XSS Le JavaScript côté navigateur (affectation à innerHTML, eval, document.write, etc.) Le fragment d’URL, postMessage, une valeur reçue du serveur, etc.

Dans cette question, le serveur intégrait tel quel, dans le HTML, le titre d’avis publié par l’attaquant. Le sink se trouve dans le traitement de sortie côté serveur, et cette chaîne, enregistrée en base de données, est ensuite distribuée à toute personne ouvrant la page V. C’est donc un XSS stocké.

Le JS côté navigateurl'a transmise àinnerHTML, etc.Elle était dans le HTMLconstruit par le serveurEnregistrée(distribuée à tous par la suite)Non enregistrée(uniquement pour cette requête)La chaîne de l'attaquanta été exécutée comme un scriptOù est-elle devenueexécutable ?DOM Based XSSCette chaîne est-elleenregistrée surle serveur ?XSS stockéXSS réfléchi

À noter qu’il est dangereux de simplifier en disant que « si la chaîne d’attaque figure dans la réponse du serveur, c’est un XSS stocké ou réfléchi ». Même si le serveur renvoie la valeur sous forme de texte inoffensif ou de JSON, cela devient un DOM Based XSS dès lors que le JavaScript côté navigateur l’affecte à innerHTML (c’est ce qu’on appelle le stored DOM XSS, où une valeur enregistrée en est la cause). Dans ce cas, ce qu’il faut corriger n’est pas le traitement de sortie du serveur mais le sink côté client : jugez donc non pas selon que la chaîne est visible dans la réponse, mais selon l’endroit où elle devient exécutable.

Ce que fait le script injecté (manipuler le DOM, communiquer, lire les cookies) et comment ce script s’est introduit dans la page sont deux questions qu’il faut traiter séparément. Le risque de les confondre n’est pas qu’on ait du mal à choisir une contre-mesure, mais qu’on se trompe de contre-mesure. Si l’on conclut à un DOM Based XSS, on en tire la conclusion qu’il suffit de corriger le JavaScript côté client — alors qu’ici, c’était bien le traitement de sortie côté serveur qu’il fallait corriger.

Sous-question 1(2) : la contre-mesure

La sous-question 1(2) demande de répondre, en 30 caractères maximum, quelle contre-mesure adopter dans l’application web Q. La réponse type est : « Appliquer un traitement d’échappement au titre de l’avis avant de le générer en sortie ».

Le fait que l’ordre « avant de le générer en sortie » soit explicitement précisé ici est important. On n’échappe pas au moment où l’on reçoit l’entrée, mais juste avant de la produire en HTML, selon le contexte de la destination. Le guide de l’IPA « Comment concevoir un site web sûr » cite d’ailleurs, en tête des solutions fondamentales contre le XSS, « appliquer un traitement d’échappement à tous les éléments générés en sortie sur une page web ».5

« Pour l’injection SQL, l’échappement à l’entrée n’est-il pas important ? »

C’est une question qui revient immanquablement. La conclusion d’emblée : même pour l’injection SQL, « l’échappement à l’entrée » n’est pas une contre-mesure.

Ce que l’IPA cite comme solution fondamentale contre l’injection SQL, c’est « implémenter la construction de toute instruction SQL exclusivement au moyen d’espaces réservés (placeholders) ». L’échappement est mentionné comme solution de repli lorsque l’instruction SQL doit être construite par concaténation de chaînes, mais il est précisé que « lorsque la construction de l’instruction SQL se fait par concaténation de chaînes, il faut utiliser les API du moteur de base de données assurant l’échappement, afin de construire correctement les littéraux de l’instruction SQL » — l’échappement a lieu au moment où l’on construit l’instruction SQL6, et non au moment où l’on reçoit l’entrée.

C’est donc exactement la même structure que pour le HTML. Le principe commun s’énonce ainsi :

L’échappement se fait à l’endroit où se détermine dans quel univers syntaxique la donnée va sortir.

  • Si elle sort en HTML, échappement HTML
  • Si elle devient une partie d’une instruction SQL, espace réservé (ou, à défaut, échappement en tant que littéral SQL)
  • Si elle devient une partie d’une commande shell, traitement selon les conventions du shell

Pourquoi ne peut-on pas le faire à l’entrée ? Parce qu’au moment où l’on reçoit l’entrée, on ne sait pas encore où cette donnée sortira dans le futur. Le même texte d’avis peut sortir sur une page HTML, dans un export CSV, dans une réponse JSON d’API, dans un e-mail de notification, ou dans un journal. Si l’on applique un échappement HTML dès l’entrée, la chaîne &amp; apparaît telle quelle dans le CSV, le contenu de la base de données diffère de l’entrée d’origine, et la recherche comme l’agrégation en sont faussées. Cela devient aussi un terreau propice au double échappement.

Le côté entrée ne doit-il alors rien faire ?

Non. Mais ce que fait le côté entrée, c’est la validation, pas l’échappement. Le rôle est différent.

  • « Rejeter » à l’entrée — refuser les valeurs impossibles selon la spécification. Pour un champ de code postal, rejeter tout ce qui n’est pas 7 chiffres ; pour un champ de quantité, rejeter les nombres négatifs. C’est un traitement indépendant, nécessaire pour préserver l’intégrité des données
  • « Échapper » à la sortie — représenter en toute sécurité, dans la syntaxe de la destination, la donnée qui a été acceptée. C’est la solution fondamentale contre la vulnérabilité

Il est également important de ne pas trop attendre de la validation côté entrée en matière de contre-mesure XSS. À propos du XSS, l’IPA, après avoir évoqué la méthode consistant à vérifier que la valeur d’entrée respecte la spécification de l’application, précise explicitement que l’efficacité de cette contre-mesure est limitée, qu’elle ne constitue pas une protection lorsque la spécification requise par l’application autorise une large variété de caractères en entrée, et qu’il n’est donc pas recommandé de s’y fier.5

Le titre et le détail d’avis étaient précisément, dans ce cas, en texte libre autorisant une large variété de caractères. Il existe bien une limite de 50 ou 300 caractères, mais le nombre de caractères nécessaire pour faire tourner un script est bien inférieur à cela, donc l’existence même d’une limite ne constitue pas une protection. Ce qui s’est réellement passé chez la société Q, nous allons le voir à la sous-question 2 qui suit.

5. Comment la limite de 50 caractères a été contournée — sous-question 2

La sous-question 2 est le sommet de cette question.

À propos de la figure 3, répondez en 50 caractères maximum à la méthode ayant permis d’exécuter un script dépassant la limite de caractères en entrée.

La réponse type est : « Publier en plusieurs fois, de façon à ce que le HTML soit mis en commentaire et que l’ensemble ne forme qu’un seul script».

L’attaquant a fractionné son texte en morceaux tenant chacun dans une publication, et a publié 15 fois. Le point clé est la façon de traiter le HTML (</div>, <div class="...">, etc.) qui s’intercale inévitablement entre chaque publication. Ce HTML constitue une erreur de syntaxe s’il est interprété comme du JavaScript.

C’est là qu’interviennent les commentaires de bloc de JavaScript. Voici une version simplifiée que nous avons rédigée pour expliquer le mécanisme (ce n’est pas une citation littérale de la figure du livret d’examen).

Supposons que l’on publie ceci dans le champ de titre d’avis, en trois fois.

1re publication : Magnifique<script>a=1;/*
2e publication : */b=2;/*
3e publication : */c=3;</script>

Le serveur génère cela, pour chaque avis, comme titre en HTML, de la façon suivante.

<div class="review-title">Magnifique<script>a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;</script></div>

Du point de vue du navigateur, tout ce qui va du premier <script> au dernier </script> constitue un seul élément script. Son contenu est le suivant.

a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;

Le HTML pris en sandwich entre /* et */ est ignoré comme un commentaire JavaScript, et seul a=1; b=2; c=3; est réellement exécuté. Or, la portion ignorée en tant que commentaire, étant elle aussi le contenu d’un élément script, ne s’affiche pas non plus à l’écran. C’est là l’explication réelle du symptôme « 16 avis qui n’en montrent que 2 ».

Ce qu’il faut retenir de cela

Ne vous trompez pas de cause et effet ici. Si la limite de caractères en entrée n’a pas fait office de contre-mesure XSS, ce n’est pas parce que le nombre de publications était illimité. C’est parce que 50 caractères suffisent déjà, en soi, à exécuter un script. Ajouter un seul attribut gestionnaire d’événement, ou placer une seule balise chargeant un script externe, tient en quelques dizaines de caractères. Même en limitant à une seule publication, la limite de caractères n’aurait constitué aucune protection.

Alors, qu’était la publication fractionnée dans ce cas ? C’était un moyen d’introduire dans son intégralité un long script d’une vingtaine de lignes. L’attaquant avait quelque chose de long à faire, et a compensé par le nombre de publications — ce n’est pas là, en soi, la raison pour laquelle la limite de caractères a été franchie. Il faut distinguer la description du mode opératoire de l’évaluation de son efficacité en tant que contre-mesure.

La même chose vaut pour l’ensemble des autres « restrictions côté entrée ».

  • Limite de caractères, restriction des types de caractères autorisés, validation côté front-end — ce sont des éléments nécessaires selon la spécification, mais ils ne remplacent pas la solution fondamentale contre le XSS
  • L’attaquant dispose toujours d’une marge pour contourner les restrictions côté entrée, par des moyens comme le fractionnement, l’encodage, ou l’injection par un autre canal
  • L’endroit à protéger est le moment où la donnée sort en tant que HTML

Notez que le commentaire de correction de l’IPA écrit aussi, à propos de cette sous-question, qu’« on a observé, pour certaines, des réponses laissant penser à une vérification insuffisante, du type “il a supprimé la restriction d’entrée via les outils de développement avant de publier” ». Il est vrai que la restriction côté front-end peut être retirée via les outils de développement. Mais ce que cette sous-question demandait, c’était de déduire, à partir des traces laissées dans le HTML, ce qui s’est réellement passé. Dans le HTML de la figure 3, on trouvait 15 publications alignées, chacune tenant dans la limite, avec des symboles de commentaire placés au début et à la fin de chacune. Ce ne sont pas les traces d’une limite « retirée », mais celles d’une limite « contournée ». Il fallait sans doute y lire une exigence de vérifier, un par un, ce que l’attaquant avait laissé derrière lui.

La façon de concevoir jusqu’où et comment valider les valeurs d’entrée est également abordée, sous l’angle d’une conception qui ne fait pas confiance aux données venant de l’extérieur, dans « Comment une application doit-elle valider la valeur lue d’un code QR ».

6. Ce que faisait le script — sous-question 3(1)

Le script extrait par M. N compte une vingtaine de lignes. La sous-question 3(1) demande de répondre, en 60 caractères maximum, au traitement effectué par les lignes 6 à 20 (la partie exécutée après le succès de la première communication).

La réponse type est : « Téléverser l’identifiant de session sous forme d’image d’icône, accompagné du jeton obtenu depuis la réponse de la requête XHR».

Voici un code équivalent que nous avons rédigé pour expliquer ce comportement (ce n’est pas une citation littérale de la figure du livret d’examen).

// ① Récupérer d'abord la page de paramètres du profil
const xhr = new XMLHttpRequest();
xhr.open("get", "https://example.jp/user/profile");
xhr.responseType = "document";   // Recevoir la réponse comme un DOM plutôt que comme du texte
xhr.send();

xhr.onload = function () {
  // ② Lire le jeton de téléversement par la même procédure que l'écran légitime
  const token = xhr.response.getElementById("token").value;

  // ③ Faire du cookie (contenant l'identifiant de session) le contenu tel quel d'un fichier PNG
  const file = new File([document.cookie], "a.png", { type: "image/png" });

  // ④ Envoyer vers la fonction de téléversement d'icône du site lui-même
  const form = new FormData();
  form.append("uploadfile", file);
  form.append("token", token);

  const xhr2 = new XMLHttpRequest();
  xhr2.open("post", "https://example.jp/user/upload");
  xhr2.send(form);
};

Les cinq premières lignes n’existent que pour franchir la contre-mesure CSRF de la société Q

Ce qu’il faut remarquer dans ce script, c’est que toute la première moitié est consacrée à l’obtention du jeton.

Pour le téléversement de l’icône, la société Q exigeait que « le jeton corresponde à celui délivré par /user/profile ». C’est un mécanisme destiné à empêcher qu’un formulaire falsifié, placé sur un autre site, ne déclenche un téléversement à l’insu de l’utilisateur — autrement dit, une contre-mesure CSRF.

Mais le script d’attaque s’exécute, dans le navigateur de la victime, sur la même origine que la victime. Il suffit alors de faire exactement ce que fait l’écran légitime : envoyer un GET vers la page de paramètres du profil, et lire la valeur de l’élément token qui s’y trouve. Cela suffit à obtenir le jeton.

Le jeton de la contre-mesure CSRF ne protège pas contre le XSS. Car dès qu’un XSS réussit, le code de l’attaquant peut se comporter comme « un utilisateur légitime déjà connecté ». Ce que le jeton protège, c’est « une requête envoyée à l’insu de l’utilisateur depuis une autre origine », pas « un script malveillant qui s’exécute sur la même origine ».

Cette distinction est également utile en pratique lorsqu’on lit des exigences de sécurité. L’explication « on a mis un jeton CSRF, donc tout va bien » est correcte pour le CSRF, mais ne dit rien du tout sur le XSS.

Ce que signifie l’objet File

La ligne ③ mérite aussi qu’on s’y arrête. Elle crée un objet fichier dont le contenu est la chaîne document.cookie, avec pour nom de fichier a.png et pour type MIME image/png.

Le contenu n’est qu’un simple texte. Ce n’est pas un fichier PNG. Si le téléversement a malgré tout réussi, c’est parce que, comme l’indique l’énoncé, l’application web Q ne vérifiait pas le format des fichiers image téléversés.

Et si document.cookie a pu être lu, c’est parce que l’attribut HttpOnly n’avait pas été ajouté au cookie. La RFC 6265 définit l’attribut HttpOnly ainsi : « limite la portée du cookie aux requêtes HTTP. En particulier, il ordonne à l’agent utilisateur d’omettre le cookie lorsqu’il fournit un accès aux cookies via des API “non HTTP”, telles que les API de navigateur web exposant les cookies à des scripts. »7 Si cet attribut avait été présent, seul l’identifiant de session aurait manqué dans la chaîne capturée par la ligne ③, la rendant sans valeur même exfiltrée.

Il faut noter ici que HttpOnly ne masque que les cookies portant cet attribut. S’il existe, sur la même origine, des cookies sans HttpOnly (préférences d’affichage, identifiants de mesure d’audience, etc.), document.cookie continue de les renvoyer. Ajouter HttpOnly ne signifie pas que « document.cookie devient vide ». Ce que l’on veut protéger, c’est l’identifiant de session : vérifiez donc individuellement s’il est bien porté par le cookie de l’identifiant de session.

7. Une exfiltration qui ne sort jamais du site — sous-questions 3(2) et 3(3)

La sous-question 3(2) demande, en 50 caractères maximum, comment l’attaquant peut récupérer l’information téléversée. La réponse type est : « Télécharger l’image d’icône du membre, puis en extraire la chaîne de l’identifiant de session».

Rappelez-vous : la spécification de l’énoncé indiquait ceci.

L’icône téléversée s’affiche sur la page de paramètres du profil membre et sur les pages d’avis.

Autrement dit, « l’image d’icône » où a été écrit l’identifiant de session de la victime se retrouve à un endroit consultable sur le site. L’attaquant n’a rien de particulier à faire. Il lui suffit d’ouvrir une page où s’affiche l’icône de la victime, et de récupérer l’URL de cette image. Le contenu n’étant que du texte, l’identifiant de session est lisible dès qu’on l’ouvre.

Précisons que cette récupération se fait sans difficulté lorsque l’icône de la victime apparaît sur une page visible par l’attaquant (par exemple la page d’avis d’un produit sur lequel il a lui-même publié un avis). L’URL de l’icône étant fixée pour chaque membre, une fois qu’elle a été aperçue quelque part, on peut ensuite la récupérer directement. Dans tous les cas, l’opération de l’attaquant se limite à envoyer des GET sur des pages ou des images en tant que visiteur légitime, ce qui, du point de vue du site, ne constitue en rien une communication anormale.

Navigateur de la victimeApplication web Q(example.jp)AttaquantNavigateur de la victimeApplication web Q(example.jp)AttaquantPhase de préparationEnregistre la chaîne publiéetelle quelle(cet enregistrement est correct en soi)La victime ouvre la page V★C'est ici la vulnérabilité★intègre le titre enregistrédans le HTML sans l'échapperExécuté comme élément scriptLit document.cookieet en fait le contenu de a.pngEnregistre sans vérifier le formatet publie comme image d'icôneRécupérationPublie l'avis en 15 fois(fait sauter le HTML avec des symboles de commentaire)1GET page V2HTML contenant le script d'attaque3GET /user/profile4HTML contenant le jeton5POST /user/upload(uploadfile=a.png, token=…)6GET l'image d'icône de la victime7Fichier contenant l'identifiant de session8

Les contre-mesures de sortie ne suffisent pas à l’arrêter

Ce qui rend ce chemin problématique devient évident une fois mis en schéma : du début à la fin, le navigateur de la victime ne communique qu’avec le site légitime (example.jp).

Par conséquent, tout type de contre-mesure surveillant les communications échoue systématiquement. Une seule, la dernière, est efficace.

Contre-mesure Face à cette attaque Raison
Filtrage d’URL par proxy Inefficace La destination n’est que le site e-commerce légitime pour l’activité
Surveillance des communications sortantes par pare-feu Inefficace Aucune communication ne se produit vers un domaine inconnu
Restriction connect-src de la Content Security Policy (directive autorisant la même origine, comme 'self') Inefficace Les deux communications sont toutes deux adressées à la même origine, et restent dans le périmètre autorisé par la politique
Restriction script-src de la Content Security Policy (configuration n’autorisant pas 'unsafe-inline') Efficace Le script en ligne injecté n’est de toute façon jamais exécuté

Quand on pense à une exfiltration d’informations par XSS, on imagine volontiers un schéma où « le cookie est envoyé vers le serveur de l’attaquant ». Mais cette question démontre clairement que la destination de l’exfiltration peut être le site victime lui-même. Dès lors qu’il existe, à l’intérieur du site, un emplacement à la fois inscriptible et lisible, celui-ci devient un point de remise. Les fonctions de téléversement de fichiers, les champs de profil public, ou les champs de notes publiques en sont des exemples typiques.

Précision sur la CSP : parmi les directives couramment utilisées en pratique, celle qui arrête cette attaque avec certitude n’est pas connect-src (restriction des destinations de communication) mais script-src. Avec une configuration script-src 'self' n’autorisant pas 'unsafe-inline', le <script> en ligne injecté dans l’avis ne serait de toute façon jamais exécuté. Si l’on comprend la CSP uniquement comme « ce qui restreint les communications vers l’extérieur », on passe à côté de cette différence.

connect-src, de son côté, n’est pas non plus dénué de tout effet par principe. Une directive interdisant même la communication vers la même origine (comme connect-src 'none'), ou une liste d’autorisation restreignant les destinations à des points de terminaison spécifiques, arrêterait elle aussi ces deux requêtes XHR. Mais peu de sites peuvent se permettre une restriction aussi poussée, si bien que script-src reste, en pratique, la mesure de référence. La compréhension exacte n’est pas « la même origine échappe au périmètre de la CSP », mais plutôt « avec une directive courante autorisant la même origine, cela passe ».

Sous-question 3(3) : ce que permet l’identifiant de session

La sous-question 3(3) demande, en 40 caractères maximum, ce que permet de faire l’information récupérée. La réponse type est : « Se faire passer pour le membre ayant accédé à la page V, et utiliser les fonctions de l’application web Q».

Selon le commentaire de correction, le taux de bonnes réponses à cette sous-question était élevé. Il y est indiqué que l’impact, pour un site e-commerce, de la récupération d’un cookie par un attaquant était bien compris.

Et en relisant la spécification de l’énoncé, on constate que la fonction de profil membre comprenait une page d’enregistrement des informations de carte bancaire, explicitement réservée aux membres connectés. L’énoncé avait prévu, dès le départ, ce qui se trouve au bout de l’usurpation d’identité.

8. Pourquoi l’attaque ne fonctionne pas depuis le site de l’attaquant — sous-question 4

La sous-question 4 est la question la plus fondamentale de cette grande question.

À supposer que l’attaquant prépare, sur un site situé sur un domaine qu’il a lui-même préparé, un HTML contenant le même script que la figure 4, et qu’un membre connecté de l’application web Q accède à ce site, l’attaque n’aboutirait pas, en raison du fonctionnement du navigateur web. Répondez en 40 caractères maximum à ce mécanisme.

La réponse type est : « Le mécanisme par lequel le cookie n’est pas envoyé, depuis un script, vers une URL d’un autre domaine».

Supposons que l’on place le même script sur evil.example, et qu’on le fasse exécuter par un membre de la société Q. Que se passe-t-il ?

D’abord, la première communication échoue à vide. Même si le script hébergé sur evil.example envoie une XMLHttpRequest vers https://example.jp/user/profile, il s’agit d’une requête cross-origin.

Ce qui est déterminant, c’est que le cookie n’est pas joint. Comme le script de la figure 4 ne spécifie pas withCredentials, aucun cookie n’accompagne la requête cross-origin. Du point de vue du serveur de la société Q, il s’agit d’un accès non connecté, et la page de paramètres du profil contenant le jeton n’est pas renvoyée. Le script ne peut pas obtenir le jeton, et s’arrête là.

Il existe un autre obstacle : l’impossibilité de lire la réponse. Si la société Q n’autorise pas evil.example via Access-Control-Allow-Origin, le navigateur échoue à la vérification CORS et traite cette communication comme une erreur réseau ; onload ne se déclenche pas et xhr.response reste inaccessible. Un site ordinaire ne renvoyant pas cet en-tête pour une origine inconnue, l’attaque devrait, en pratique, s’arrêter ici aussi.

Cela dit, la façon dont la société Q avait configuré CORS n’est pas précisée dans l’énoncé. Même en supposant qu’elle autorisait l’accès depuis evil.example, ce qui serait lisible serait une réponse en état non connecté, sans jeton — l’attaque n’aboutirait donc pas non plus. Le seul premier point, l’absence de cookie joint, suffit déjà à arrêter l’attaque.

L’un ou l’autre suffirait à arrêter le script, mais en réalité, les deux jouent.

De plus, le cookie lui-même n’est pas lisible. Ce que renvoie document.cookie, ce sont les cookies de l’origine sur laquelle le script s’exécute (evil.example). L’identifiant de session délivré pour example.jp ne s’y trouve pas.

La réponse type de l’IPA retient le premier point, à savoir « le cookie n’est pas envoyé vers une URL d’un autre domaine ».

Ne retenez surtout pas ces deux points comme un seul et même mécanisme. Ce sont des mécanismes distincts.

  • Si document.cookie ne renvoie pas les cookies de example.jp, c’est parce que les cookies sont gérés séparément par domaine. Entre des sites distincts (des domaines enregistrables différents) comme evil.example et example.jp, cette séparation ne peut pas être assouplie par une quelconque configuration. Il en va toutefois autrement entre sous-domaines d’un même domaine : en utilisant l’attribut Domain du cookie, on peut le partager entre sub.example.jp et example.jp.7
  • Si aucun cookie n’accompagne une XHR cross-origin, c’est parce que withCredentials vaut false par défaut. Ce point-ci peut changer si les conditions sont réunies. Si le script spécifie withCredentials = true, que l’attribut SameSite du cookie autorise l’envoi cross-site, et que le serveur renvoie un Access-Control-Allow-Origin désignant explicitement l’origine d’envoi ainsi que Access-Control-Allow-Credentials: true, alors le cookie est envoyé et la réponse devient lisible

Distinguez bien ici la raison de l’échec réel et ce qui serait nécessaire si l’on modifiait le script.

Une seule chose peut être affirmée avec certitude à partir de l’énoncé et de la figure 4 : withCredentials n’étant pas spécifié, aucun cookie n’est joint, l’accès est traité comme non connecté, et le jeton n’est pas obtenu. Cela suffit à lui seul à arrêter l’attaque.

L’absence d’autorisation CORS constitue également un obstacle empêchant de lire la réponse, mais comme la configuration de la société Q n’est pas précisée dans l’énoncé, considérez ce point comme un renfort conditionnel — « c’est généralement le cas pour un site ordinaire ».

Alors, que se passerait-il si l’attaquant réécrivait le script avec withCredentials = true ? C’est seulement à ce moment-là qu’une autre condition entre en jeu. Si le SameSite du cookie n’autorise pas l’envoi cross-site, le cookie ne sera de toute façon pas joint, même en activant withCredentials. Les navigateurs actuels traitant un cookie sans SameSite spécifié comme Lax, la situation penche par défaut du côté du non-envoi.

Précisons que la façon dont SameSite était configuré sur le cookie d’identifiant de session de la société Q n’est pas indiquée dans l’énoncé. On ne peut donc pas affirmer qu’on « était protégé grâce à SameSite ». Il s’agit uniquement du fait que SameSite peut jouer un rôle face à un adversaire qui attaque avec des identifiants (credentials).

En résumé, pour lire depuis un autre domaine une réponse authentifiée, trois conditions doivent être réunies : withCredentials = true, un SameSite de cookie autorisant l’envoi cross-site, et un serveur autorisant l’origine d’envoi via un CORS avec identifiants. Un domaine différent ne garantit pas automatiquement la sécurité, mais il ne suffit pas non plus qu’une seule condition soit réunie pour que tout s’effondre. Lorsqu’on inspecte son propre site, plutôt que de se rassurer sur la seule séparation des cookies par domaine, il faut vérifier concrètement la configuration CORS (en particulier quelles origines sont autorisées pour les requêtes avec identifiants) et l’attribut SameSite du cookie.

D’où la forte valeur du XSS stocké

Ce que nous enseigne cette sous-question, c’est la raison pour laquelle un attaquant tient tant à « faire tourner son propre code à l’intérieur du site victime ».

Même en préparant, sur son propre domaine, un faux site visuellement identique, le navigateur le traite comme un site distinct. Impossible de lire les cookies du site cible depuis document.cookie, et avec une XHR envoyée sans identifiants, comme dans le script de cette sous-question, on n’est même pas considéré comme connecté du point de vue du site cible. La valeur, pour un attaquant, réside précisément dans le fait que le code s’exécute sur l’origine du site victime. Le XSS stocké est exactement le moyen qui rend cela possible.

Ne lisez pas cela, cependant, comme signifiant qu’« on ne peut absolument jamais envoyer de requête authentifiée depuis un autre domaine ». Si le SameSite du cookie autorise l’envoi cross-site (par exemple SameSite=None), une page malveillante peut tout à fait faire parvenir une requête accompagnée du cookie jusqu’au serveur cible. Le POST d’un formulaire en est l’exemple typique ; ce que contrôle principalement CORS, c’est « si le script peut lire la réponse », pas « si la requête parvient au serveur » (à ceci près que, pour l’ajout d’en-têtes personnalisés ou un envoi en application/json, une requête préliminaire — preflight — la précède, et si elle n’est pas autorisée, la requête elle-même n’est jamais envoyée). C’est la raison pour laquelle l’attaque CSRF fonctionne, et c’est précisément pourquoi le jeton de contre-mesure CSRF est nécessaire.

Autrement dit, ce qu’un script d’une autre origine ne peut pas faire par défaut, c’est « lire le cookie de la cible » et « lire la réponse » — pas « envoyer la requête ».

Et ces deux « impossibilités de lire » n’ont pas la même force. La séparation par domaine de document.cookie ne peut pas être assouplie par une configuration côté serveur, mais la possibilité de lire la réponse dépend entièrement de la configuration CORS du serveur cible. Si le serveur autorise l’origine d’envoi (en renvoyant une origine explicite dans Access-Control-Allow-Origin, accompagnée de Access-Control-Allow-Credentials: true pour les requêtes avec identifiants), un script d’une autre origine peut lire la réponse. C’est pour cela qu’il faut vérifier la configuration CORS de son propre site.

Ce qui rend le XSS particulier, c’est qu’il permet de sauter par-dessus toutes ces conditions, en se comportant d’emblée comme s’il était sur l’origine du site victime.

Autrement dit, le XSS n’est pas « un bug qui fait apparaître des caractères bizarres à l’écran », mais un bug qui place l’attaquant dans la même position qu’un utilisateur légitime du site. Cette différence de perception change la façon de juger de la priorité.

9. Les contre-mesures qui ont fonctionné, et celles qui n’ont pas fonctionné

Voici un tableau qui résume tout ce qui précède. C’est la partie de cet article qu’il faut le plus retenir.

Mécanisme présent/absent chez la société Q Face à cette attaque Raison
Communication en HTTPS Inefficace Protège le canal de communication, sans rapport avec le script injecté dans la page
Limite de caractères en entrée (50 pour le titre, 300 pour le détail) Inefficace Publié en 15 fois, le HTML intercalé étant ignoré via des commentaires JavaScript
Jeton de téléversement (contre-mesure CSRF) Inefficace Un script de la même origine peut obtenir le jeton par la procédure normale
Fonction de téléversement réservée aux membres connectés Inefficace C’est le navigateur de la victime elle-même, déjà connectée, qui exécute le script
Échappement à la sortie (absent) Efficace (solution fondamentale) La chaîne publiée n’est plus interprétée comme du HTML, le script ne s’exécute donc jamais
Attribut HttpOnly du cookie (absent) Efficace (contre-mesure assurance) L’identifiant de session ne peut plus être lu via document.cookie
Vérification du format du fichier téléversé / réencodage (absent) Efficace (contre-mesure assurance) Un texte brut ne peut pas être enregistré comme PNG, et une chaîne cachée dans les métadonnées disparaît au réencodage. Elle subsiste toutefois si elle est encodée directement dans les pixels de l’image

Les quatre premières lignes n’ont eu aucun effet sur cette attaque. En appliquant aux trois dernières la classification du guide « Comment concevoir un site web sûr » de l’IPA, la première est une solution fondamentale, et les deux suivantes des contre-mesures assurance.5

Une précision : les quatre premières ne signifient pas que « l’IPA ne les reconnaît pas comme des contre-mesures ». La classification de l’IPA compte deux catégories, solution fondamentale et contre-mesure assurance, et la vérification du contenu des valeurs d’entrée est même citée comme contre-mesure assurance. Mais elle est assortie de la réserve que « l’efficacité de cette contre-mesure est limitée » — et c’est précisément cette limite qui s’est vérifiée cette fois-ci.

Et ce qui compte, c’est que n’importe laquelle des trois dernières, à elle seule, aurait suffi à briser la chaîne de l’attaque exécutée dans cette question.

  • Avec l’échappement, le script ne serait de toute façon jamais exécuté
  • Avec HttpOnly, le script s’exécuterait mais ne pourrait pas lire le cookie
  • Avec la vérification du format, le cookie serait lisible, mais le texte brut ne pourrait pas être téléversé comme PNG

C’est précisément dans ce genre de situation que la défense en profondeur prend tout son sens. Les contre-mesures assurance, cependant, ne remplacent pas la solution fondamentale. Même avec HttpOnly, si le XSS subsiste, l’attaquant peut exécuter n’importe quelle opération (achat de produit, modification des informations enregistrées, etc.) dans le navigateur de la victime. On peut concevoir autant d’attaques qu’on veut qui n’ont même pas besoin de voler l’identifiant de session.

Pourquoi mentionner le « réencodage » et pas seulement la « vérification du format »

Il y a une raison pour laquelle la dernière ligne du tableau est écrite en deux temps, « vérification du format / réencodage ».

Comme la société Q de l’énoncé ne vérifiait absolument pas le format des fichiers image, ajouter une simple vérification de format suffirait à arrêter cette attaque. Mais cela ne vaut que dans le cas où l’attaquant envoie du texte brut. Un attaquant peut aussi construire un fichier PNG parfaitement valide et y dissimuler une chaîne, par exemple dans sa zone de métadonnées. Dans ce cas, la vérification « le format est-il correct ? » serait passée avec succès.

Pour restreindre ce chemin, il faut donc aller jusqu’à réencoder côté serveur l’image téléversée avant de la diffuser (ne pas conserver telle quelle la suite d’octets d’origine).

Le réencodage lui-même ne suffit pas non plus à tout bloquer. Si l’attaquant crée un PNG valide en dessinant l’identifiant de session directement dans les pixels eux-mêmes de l’image, un réencodage classique préservera l’apparence visuelle, et l’attaquant pourra donc la télécharger et la lire. Ce que le réencodage arrête, ce sont les variantes envoyant des octets bruts et celles dissimulées dans la zone de métadonnées — pas l’information encodée dans le contenu même de l’image.

Autrement dit, tant qu’il existe, à l’intérieur du site, un « endroit à la fois inscriptible et lisible », on ne peut jamais fermer complètement ce chemin d’exfiltration. Cela illustre bien la nature des contre-mesures assurance : il ne suffit pas d’arrêter « l’attaque actuellement observée », il faut concevoir en se demandant aussi si la protection tient encore lorsque l’attaquant modifie légèrement son mode opératoire. Et quel que soit le nombre de couches empilées, cela ne remplace jamais la solution fondamentale qu’est l’échappement à la sortie.

Notez que la conception consistant à diffuser les fichiers téléversés depuis un domaine distinct du site principal est souvent recommandée, mais ce n’est pas une contre-mesure contre ce chemin d’exfiltration. L’attaquant peut télécharger l’image de la même façon depuis un autre domaine, et la politique de même origine n’est pas un mécanisme qui garde secrète une suite d’octets rendue publique. La diffusion depuis un domaine distinct est efficace contre une menace différente : celle où le fichier téléversé lui-même s’exécute comme contenu actif sur l’origine du site victime (le cas d’un HTML ou d’un SVG téléversé). Comprenez-la comme une contre-mesure séparée, pour une menace distincte.

L’idée de juger un fichier par son contenu plutôt que par la déclaration de l’expéditeur (extension, Content-Type) est un comportement de base nécessaire, pas seulement sur le Web. Mais ce que signifie « juger par le contenu » varie selon le format. Pour un format comme PNG, qui possède une séquence d’octets fixe en tête de fichier, on peut la vérifier. Le CSV, en revanche, n’a pas de signature normalisée ; le BOM en tête indique seulement l’encodage des caractères et ne prouve en rien que le contenu est bien du CSV. Comme abordé dans « Le traitement des fichiers CSV », pour le CSV, il faut vérifier que l’analyse réussit avec le dialecte attendu, et que le schéma et le nombre de lignes restent dans les limites prévues.

10. Points à vérifier dans son propre code

En transposant cette grande question en liste de contrôle pour une revue de code, on obtient ce qui suit. Directement utilisable pour toute application web dotée de fonctions de compte membre ou de publication.

  1. L’échappement à la sortie est-il actif à tous les points de sortie ? Recensez tous les endroits où l’échappement automatique du moteur de template a été explicitement désactivé (raw, | safe, dangerouslySetInnerHTML, etc.), et vérifiez, pour chacun, que l’on peut justifier la raison de cette désactivation
  2. Les restrictions côté entrée sont-elles comptées, à tort, comme une contre-mesure XSS ? La limite de caractères ou la restriction des types de caractères autorisés sont des exigences de spécification, pas la solution fondamentale contre le XSS
  3. Échappe-t-on à l’entrée ? Le rôle de l’entrée est la validation, qui rejette les valeurs hors spécification, pas l’échappement. Comme la destination (HTML, CSV, JSON, e-mail, journal) n’est pas encore déterminée au moment de l’entrée, échapper à l’entrée provoque un double échappement et corrompt les données. Il en va de même pour la construction des instructions SQL : la solution fondamentale, ce sont les espaces réservés
  4. Les cookies portent-ils HttpOnly, Secure et SameSite ? Vérifiez tout particulièrement le cookie de l’identifiant de session
  5. Le format des fichiers téléversés est-il vérifié par leur contenu, et non par l’extension ou le Content-Type ? L’information déclarée par l’expéditeur ne constitue pas une base de vérification. Mais la seule vérification du format laisse passer la technique consistant à dissimuler des données dans la zone de métadonnées d’une image valide
  6. Les images téléversées sont-elles réencodées avant diffusion ? Ne renvoie-t-on pas telle quelle la suite d’octets d’origine ? Mais gardez à l’esprit que même le réencodage laisse subsister l’information encodée dans les pixels, et qu’un « endroit inscriptible et lisible » ne peut donc jamais être complètement fermé. Notez aussi que la diffusion depuis un domaine distinct est une contre-mesure contre la menace où le fichier téléversé s’exécute sur l’origine de son propre site, pas contre ce chemin d’exfiltration
  7. La configuration CSP est-elle capable d’arrêter les scripts en ligne ? Le critère de jugement n’est pas « 'unsafe-inline' est-il écrit ? » mais « les scripts en ligne sont-ils réellement autorisés ? ». Lorsque script-src comporte une spécification nonce ou hash, les navigateurs supportant CSP Level 2 et ultérieur ignorent 'unsafe-inline' ; ainsi, avec une politique rétrocompatible mentionnant à la fois 'unsafe-inline' et un nonce, un script injecté sans nonce sera bloqué
  8. Peut-on expliquer ce que le jeton protège précisément ? Le jeton de contre-mesure CSRF ne protège pas contre le XSS. Chacun nécessite une contre-mesure distincte

Le premier point, en particulier, correspond à un schéma typique rencontré lors d’investigations réelles. Il n’est pas rare de découvrir un cas où l’on pensait être protégé grâce à l’échappement automatique du framework, alors qu’à un seul endroit, pour un besoin ponctuel du type « je veux insérer du HTML tel quel », cet échappement automatique avait été désactivé.

Conclusion — la véritable compétence que teste cette question

La liste des contre-mesures passées en revue jusqu’ici est résumée dans la « conclusion » en tête d’article. Pour terminer, une dernière remarque sur cette question elle-même.

Ce qui fait la qualité de cette question d’examen, ce n’est pas qu’elle demande « connaissez-vous le XSS », mais qu’elle demande de savoir distinguer, parmi les contre-mesures dont on dispose, lesquelles sont réellement efficaces.

La société Q n’était pas restée sans rien faire. Elle communiquait en HTTPS, imposait une limite de caractères en entrée, exigeait un jeton pour le téléversement, et réservait la fonction aux membres connectés. Mise bout à bout, cette liste donne l’impression d’une construction raisonnablement sécurisée. Et pourtant, tout a été contourné. À l’inverse, les trois éléments manquants sont tous discrets — pas le genre de fonctionnalité qu’on mentionnerait dans des notes de version.

Ce qui rend les discussions sur la sécurité difficiles, c’est que le nombre de contre-mesures n’est pas proportionnel à la force de la défense. Savoir dire, une par une, non pas « ce que l’on fait », mais « quelle étape de quelle attaque telle contre-mesure arrête » — ce n’est pas une compétence utile uniquement pour un examen de certification. C’est le jugement le plus nécessaire, aussi bien pour celui qui reçoit une explication de mesures de sécurité que pour celui qui les met en œuvre.

Domaines de conseil associés

合同会社小村ソフト (Komura Software LLC) prend en charge la réalisation de sites web dotés de fonctions de compte membre ou de publication, ainsi que la revue de conception visant à vérifier qu’une application web existante ne présente pas la même faille.

Références

  1. IPA (Agence de promotion des technologies de l’information, organisme administratif indépendant), Livret d’examen, barème, réponses types et commentaire de correction (année 2023, Reiwa 5), section « Examen de spécialiste certifié en sécurité de l’information, session d’automne Reiwa 5, questions de l’après-midi ». Sur les fonctions de l’application web Q de la société Q (inscription des membres, connexion et délivrance d’un identifiant de session par cookie, limite de caractères en entrée de la fonction d’avis produit — 50 caractères pour le titre, 300 pour le détail —, téléversement d’icône et enregistrement d’informations de carte bancaire dans la fonction de profil membre, le fait que le téléversement d’icône envoie un fichier image et un jeton en paramètres, le fait que l’icône téléversée s’affiche sur la page de paramètres du profil membre et sur les pages d’avis), sur l’incident où seuls 2 avis sur 16 s’affichaient sur la page V, sur le fait que le HTML de la page V contenait 15 publications du membre A et un long script, sur le contenu du script extrait, et sur le fait que l’application web Q présentait une vulnérabilité permettant l’exécution d’un script saisi par un membre, qu’aucun attribut HttpOnly n’était attribué au cookie, et que le format des fichiers image téléversés n’était pas vérifié. Les énoncés des sous-questions 1 à 4 proviennent également de ce livret.  2

  2. IPA (Agence de promotion des technologies de l’information, organisme administratif indépendant), Questions fréquentes sur les examens. Sur le fait que, concernant l’utilisation des questions d’examens passés publiées par l’agence, aucune autorisation ni redevance n’est nécessaire sauf disposition légale particulière, que les droits d’auteur ne sont toutefois pas abandonnés, que la source doit être indiquée sous la forme « année, période, catégorie d’examen, tranche horaire, numéro de question, etc. » (l’exemple donné étant « Source : Examen d’ingénieur fondamental en technologies de l’information, session de printemps de l’année Heisei 31, matin, question 1 »), et que toute modification apportée à une partie de l’énoncé doit également être signalée. 

  3. IPA (Agence de promotion des technologies de l’information, organisme administratif indépendant), Réponses types de l’examen de spécialiste certifié en sécurité de l’information, session d’automne Reiwa 5. Sur l’objectif de la question 1 (à partir d’un cas de réponse à incident consécutif à l’exploitation d’une vulnérabilité d’un programme d’application web, tester la capacité à décrypter, à partir du HTML et de l’ECMAScript, la vulnérabilité exploitée et les problèmes rencontrés, puis à concevoir des contre-mesures), et sur les réponses types de chaque sous-question (sous-question 1(1) : « XSS stocké » ; sous-question 1(2) : « Appliquer un traitement d’échappement au titre de l’avis avant de le générer en sortie. » ; sous-question 2 : « Publier en plusieurs fois, de façon à ce que le HTML soit mis en commentaire et que l’ensemble ne forme qu’un seul script. » ; sous-question 3(1) : « Téléverser l’identifiant de session sous forme d’image d’icône, accompagné du jeton obtenu depuis la réponse de la requête XHR. » ; sous-question 3(2) : « Télécharger l’image d’icône du membre, puis en extraire la chaîne de l’identifiant de session. » ; sous-question 3(3) : « Se faire passer pour le membre ayant accédé à la page V, et utiliser les fonctions de l’application web Q. » ; sous-question 4 : « Le mécanisme par lequel le cookie n’est pas envoyé, depuis un script, vers une URL d’un autre domaine »).  2

  4. IPA (Agence de promotion des technologies de l’information, organisme administratif indépendant), Commentaire de correction de l’examen de spécialiste certifié en sécurité de l’information, session d’automne Reiwa 5. Sur le fait que le taux de bonnes réponses de l’ensemble de la question 1 était dans la moyenne, sur le fait qu’à propos de la sous-question 1(1), « on a observé un certain nombre de candidats répondant à tort “DOM Based XSS”, sans doute parce que le script utilisait le DOM », sur la remarque appelant à comprendre précisément les vulnérabilités, y compris leurs caractéristiques et leurs contre-mesures, sur le fait qu’à propos de la sous-question 2, « on a observé, pour certaines, des réponses laissant penser à une vérification insuffisante, du type “il a supprimé la restriction d’entrée via les outils de développement avant de publier” », sur la remarque appelant à cultiver la capacité à vérifier attentivement les traces laissées par l’attaquant afin de saisir avec précision la méthode d’attaque, et enfin sur le fait que le taux de bonnes réponses à la sous-question 3(3) était élevé, l’impact de la récupération d’un cookie par un attaquant sur un site e-commerce étant bien compris.  2

  5. IPA (Agence de promotion des technologies de l’information, organisme administratif indépendant), Comment concevoir un site web sûr. Sur le fait que, pour 11 types de vulnérabilités de sites web, les menaces et contre-mesures y sont présentées en distinguant « solution fondamentale » (implémentation qui supprime la cause même de la vulnérabilité) et « contre-mesure assurance » (contre-mesure qui réduit le taux de réussite de l’attaque ou les dommages si la vulnérabilité subsiste), sur le fait que l’application d’un traitement d’échappement à tous les éléments générés en sortie sur une page web est citée comme solution fondamentale contre le cross-site scripting, et sur la liste de contrôle d’implémentation de la sécurité qui l’accompagne, ainsi que sur les fascicules séparés « Comment appeler SQL en toute sécurité » et « Spécification du bilan de santé web ».  2 3

  6. IPA (Agence de promotion des technologies de l’information, organisme administratif indépendant), Comment concevoir un site web sûr - 1.1 Injection SQL. Sur le fait qu’« implémenter la construction de toute instruction SQL exclusivement au moyen d’espaces réservés » soit citée comme solution fondamentale contre l’injection SQL, que les espaces réservés statiques (instructions préparées) soient considérés comme ne pouvant, par principe, engendrer de vulnérabilité, que, pour l’implémentation dans le cas où l’instruction SQL est construite par concaténation de chaînes, il soit écrit que « lorsque la construction de l’instruction SQL se fait par concaténation de chaînes, il faut utiliser les API du moteur de base de données assurant l’échappement, afin de construire correctement les littéraux de l’instruction SQL », l’échappement s’appliquant donc à la génération du littéral qui constitue l’instruction SQL (et non au moment de la réception de l’entrée), et sur le fait que soient également citées, comme solution fondamentale, « ne pas spécifier directement d’instruction SQL dans les paramètres transmis à l’application web », et comme contre-mesures assurance, « ne pas afficher tel quel le message d’erreur dans le navigateur » et « accorder les privilèges appropriés au compte de base de données ». 

  7. IETF, RFC 6265: HTTP State Management Mechanism, section 4.1.2.6 « The HttpOnly Attribute ». Sur le fait que l’attribut HttpOnly limite la portée du cookie aux requêtes HTTP, et qu’il ordonne en particulier à l’agent utilisateur d’omettre le cookie lorsqu’il fournit un accès aux cookies via des API « non HTTP », telles que les API de navigateur web exposant les cookies à des scripts. Ainsi que sur le fait que l’attribut Secure (section 4.1.2.5) restreint l’envoi du cookie aux seuls canaux sécurisés.  2

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Création de sites web

Parce que, pour un site web doté de fonctions de compte membre ou de publication, les points abordés dans cet article — l'échappement à la sortie, la configuration des attributs de cookie — se répercutent directement sur la qualité de la réalisation.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Pourquoi le XSS de cette question est-il « stocké » ? Le script manipule le DOM, alors pourquoi n'est-ce pas un DOM Based XSS ?
Le type de vulnérabilité se détermine selon l'endroit « où » la chaîne de l'attaquant se transforme en code exécutable, c'est-à-dire selon que le point de sortie vulnérable (le sink) se trouve côté serveur ou côté navigateur. Dans cette question, le serveur renvoyait tel quel, intégré au HTML, le titre d'avis publié par l'attaquant. Le sink se trouve dans le traitement de sortie du serveur, et comme cette chaîne est enregistrée puis distribuée à toute personne ouvrant la page par la suite, il s'agit d'un XSS stocké. Le DOM Based XSS désigne le cas où le JavaScript côté navigateur rend une valeur exécutable, par exemple via une affectation à innerHTML. Que le script injecté utilise ou non des API du DOM comme XMLHttpRequest ou getElementById n'a aucun rapport avec le type de vulnérabilité. Notez que, même si le serveur renvoie une valeur enregistrée sous forme de texte inoffensif ou de JSON, cela devient un DOM Based XSS dès lors que le JavaScript côté navigateur la transmet à innerHTML. Jugez non pas selon que la chaîne est visible dans la réponse, mais selon l'endroit où elle devient exécutable. Le commentaire de correction de l'IPA relève d'ailleurs qu'on a observé un certain nombre de candidats répondant à tort « DOM Based XSS », sans doute parce que le script utilisait le DOM.
Le titre d'avis avait une limite de 50 caractères en entrée, alors comment un script aussi long a-t-il pu être exécuté ?
L'attaquant a fractionné son texte en morceaux tenant chacun dans une publication, et a publié en plusieurs fois. Dans la première publication, il ouvre une balise script au milieu du titre, puis ouvre un commentaire de bloc JavaScript (slash-astérisque) à la fin du titre. Dans la deuxième, il ferme ce commentaire au début, écrit l'instruction suivante, puis rouvre un commentaire à la fin — et répète ce schéma. Ainsi, tout le HTML intercalé entre les publications (balises div, etc.) se retrouve à l'intérieur des commentaires JavaScript et est donc ignoré, tandis que seuls les fragments fractionnés se relient pour former un seul script exécuté. La limite de caractères en entrée ne limitait que la longueur de chaque publication, pas le nombre de publications.
Où l'identifiant de session volé a-t-il été envoyé ?
Il n'a pas été envoyé vers un serveur externe. Le script a fait du contenu du cookie le contenu tel quel d'un fichier, nommé « a.png », de type MIME « image/png », qu'il a envoyé vers la fonction de téléversement d'icône de membre du site e-commerce utilisé par la victime elle-même. Comme l'icône téléversée s'affiche sur les pages d'avis et ailleurs, l'attaquant peut simplement télécharger cette image et en extraire l'identifiant de session à partir du texte qu'elle contient. Le navigateur de la victime ne communique qu'avec le site légitime, sans qu'aucune communication suspecte vers l'extérieur ne se produise, ce qui fait de ce chemin un chemin indétectable par le filtrage d'URL d'un proxy ou par la surveillance des communications sortantes d'un pare-feu.
Le téléversement était censé nécessiter un jeton — pourquoi l'attaque a-t-elle malgré tout réussi ?
Parce que ce jeton est un mécanisme destiné à rejeter les requêtes envoyées à l'insu de l'utilisateur depuis un autre site (une contre-mesure CSRF), et non une contre-mesure conçue pour un script s'exécutant sur le même site. Le script d'attaque accède d'abord à la page de paramètres du profil via XMLHttpRequest, reçoit le jeton exactement comme le ferait l'écran légitime, puis exécute le téléversement en joignant ce jeton. Grâce au XSS, le code de l'attaquant s'exécute sur la même origine que la victime, il peut donc faire tout ce qu'un utilisateur légitime peut faire. Le jeton de contre-mesure CSRF ne protège pas contre le XSS.
On dit que pour l'injection SQL, l'échappement à l'entrée est important. Est-ce que seul le XSS se traite à la sortie ?
Non, même pour l'injection SQL, « l'échappement à l'entrée » n'est pas une contre-mesure. Ce que l'IPA cite comme solution fondamentale, c'est « implémenter la construction de toute instruction SQL exclusivement au moyen d'espaces réservés ». L'échappement est mentionné comme solution de repli lorsque l'instruction SQL est construite par concaténation de chaînes, mais là aussi, il sert à « construire correctement le littéral de l'instruction SQL » — c'est-à-dire qu'il intervient au moment où l'on construit l'instruction SQL, pas au moment où l'on reçoit l'entrée. Le principe commun est que « l'échappement se fait à l'endroit où se détermine dans quel univers syntaxique la donnée va sortir » : échappement HTML si elle sort en HTML, espace réservé si elle devient une partie d'une instruction SQL, conventions du shell si elle devient une partie d'une commande shell. La raison pour laquelle il ne faut pas échapper à l'entrée est qu'à ce moment-là, la destination de sortie n'est pas encore déterminée. La même donnée sortant à la fois sur une page HTML, dans un CSV, dans du JSON, dans un e-mail de notification et dans un journal, appliquer un échappement HTML dès l'entrée fait apparaître des entités telles quelles dans le CSV, et rend le contenu de la base de données différent de l'entrée d'origine, faussant recherche et agrégation. Cela ne signifie pas pour autant que le côté entrée ne doit rien faire : son rôle est la validation, pas l'échappement — rejeter les valeurs impossibles selon la spécification.
Quelle contre-mesure faut-il retenir de cette question pour la pratique professionnelle ?
La solution fondamentale consiste à appliquer un traitement d'échappement, juste avant la génération en HTML, à toutes les entrées utilisateur, y compris le titre d'avis. À cela s'ajoutent, comme contre-mesures assurance, l'ajout de l'attribut HttpOnly au cookie de l'identifiant de session pour empêcher sa lecture par un script, le réencodage côté serveur des images téléversées pour ne pas conserver la suite d'octets d'origine, et une configuration de Content Security Policy capable d'arrêter l'exécution de scripts en ligne. Dans cette question, la société Q aurait vu la chaîne de l'attaque brisée quelque part si elle avait implémenté ne serait-ce que l'une de ces mesures. Les contre-mesures assurance, cependant, peuvent être contournées si l'attaquant change de méthode, et ne remplacent donc pas l'échappement, qui reste la solution fondamentale.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog