loader

Kit cyber - Chapitre 12 : Injection de code malveillant

Troisième chapitre du parcours injection de code du kit cyber

Dans les chapitres précédents, nous avons mis en place une calculatrice à distance : une carte cliente envoie un texte contenant un calcul, et une carte serveur exécute ce texte pour renvoyer un résultat. Nous avons aussi vu qu’en modifiant légèrement le message, il est possible d’ajouter des instructions supplémentaires et de détourner ainsi le fonctionnement prévu de la calculatrice.
Si l’on regarde maintenant notre programme serveur avec un œil plus critique, on remarque qu’il ne fait pas vraiment de différence entre les messages reçus. Qu’il s’agisse d’une expression comme 2+3, d’un calcul accompagné d’instructions supplémentaires, ou même d’un texte beaucoup plus étrange, le serveur tente toujours de l’exécuter de la même manière.

Autrement dit, dès qu’un message arrive, il est considéré comme « acceptable » par défaut. Ce n’est qu’au moment de l’exécution que les problèmes apparaissent :
  • soit le texte est bien une expression mathématique et tout se passe comme prévu
  • soit il contient du code supplémentaire qui sera aussi exécuté
  • soit il provoque des erreurs ou des comportements inattendus

Cette observation met en évidence un point important : notre programme ne précise nulle part ce qu’est un message « correct » et ce qu’il faudrait refuser. Tant que l’utilisateur joue le jeu, le système fonctionne, mais dès qu’on envoie quelque chose d’un peu différent, les limites de la calculatrice apparaissent très vite.
La question qui se pose est alors la suivante : comment faire pour que la carte serveur n’accepte que les messages correspondant réellement à un calcul, et qu’elle ignore ou rejette ceux qui pourraient poser problème ?

Le message envoyé par la carte cliente est ce que l’on appelle une entrée utilisateur : c’est une information fournie au programme depuis l’extérieur (un texte tapé au clavier, un message radio, une donnée reçue d’un formulaire, etc.). Dans notre calculatrice à distance, nous avons laissé cette entrée passer directement à une fonction d’exécution (comme exec ou un bloc « exécuter du code Python »), sans la contrôler au préalable.

C’est précisément ce choix qui rend possibles les injections de code : tant que l’entrée correspond à ce que l’on attend (un calcul simple), tout fonctionne, mais dès qu’un utilisateur envoie un texte contenant autre chose (instructions supplémentaires, caractères inattendus, fragments de programme), la fonction qui exécute le code essaie malgré tout de le traiter.
Le filtrage des entrées utilisateur consiste à ajouter une étape avant cette exécution. Cette étape a pour but de :
  • définir clairement ce que l’on considère comme une entrée valide (par exemple : un calcul composé uniquement de chiffres, d’opérateurs +, -, *, / et de parenthèses)
  • vérifier que le message reçu respecte bien ces règles
  • décider de refuser ou d’ignorer tout ce qui sort de ce cadre, au lieu d’essayer de l’exécuter coûte que coûte

On peut voir cela comme un filtre placé à l’entrée du programme : ce filtre ne laisse passer que les messages qui correspondent à une forme attendue, et bloque le reste.
Dans la suite du parcours, nous allons mettre en œuvre ce principe sur notre micro:bit : il ne s’agit plus seulement de constater que l’on peut injecter du code, mais de commencer à protéger notre calculatrice en contrôlant davantage ce que l’on accepte comme entrée utilisateur.

La solution la plus simple pour filtrer l’entrée utilisateur, dans notre cas, serait de tout simplement ignorer toutes les instructions situées après le premier retour à la ligne. Ainsi, seul le calcul est exécuté, et toutes les autres instructions sont ignorées.
L’inconvénient de cette méthode est qu’il est tout de même possible d’exécuter une instruction en remplaçant le calcul par une instruction, comme par exemple :

bloc exécution crâne

Il va donc falloir écrire une fonction qui analyse tous les caractères de la chaîne en entrée, et vérifier que ces derniers correspondent bien à ce qu’un calcul est censé contenir (chiffres, opérateurs, parenthèses).
On va tout d’abord définir une fonction nommée est_valide, qui prend en paramètre chaîne, c’est-à-dire la chaîne de caractères que l’on veut vérifier.

bloc définition est_valide

Dans cette fonction, on va tout d’abord définir une liste de caractères autorisés, qu’on va stocker dans une variable :

bloc caractères autorisés

Ensuite, on va boucler sur tous les caractères de la chaîne, et, si l’un d’eux ne correspond pas à la liste, la fonction renvoie faux. Si, une fois que tous les caractères sont analysés, aucun n’est invalide, alors la fonction renvoie vrai. Pour boucler sur les caractères d’une chaîne, on peut utiliser le bloc suivant :

bloc pour chaque élément dans la liste

La variable i va successivement prendre la valeur de chaque caractère de la chaîne chaine. Pour vérifier si un caractère est valide, on peut utiliser la méthode suivante :

bloc vérification caractère autorisé

Ce bloc permet de trouver la position de i dans caracteres_autorises. Par exemple, si i vaut 4, alors ce bloc renverra sa position, c'est-à-dire 4. En revanche, si i ne se trouve pas dans caracteres_autorises, alors ce bloc renvoie -1. On peut donc déterminer si i est un caractère valide. Si ce n’est pas le cas, on quitte la boucle et on renvoie faux, et sinon on continue. Une fois la boucle terminée, on renvoie vrai.
Enfin, il faut utiliser la fonction nouvellement créée dans le programme de la carte serveur pour vérifier si le calcul est valide avant de l’exécuter.


Si tu es bloqué :
  • Vérifie que la fonction est_valide ne laisse passer que les caractères autorisés (par exemple 0123456789+-/*()), et qu’elle retourne bien False dès qu’un caractère n’est pas dans cette liste.
  • Assure-toi que, dans la boucle principale, tu n’exécutes le calcul que si name == "CALCUL" et est_valide(value) est vrai ; sinon, le message doit être ignoré.
  • Si tous tes calculs sont rejetés, regarde si tu n’as pas mis d’espaces ou d’autres caractères dans le message envoyé par le client : ils ne font pas partie de la liste autorisée et feront échouer la validation. Tu peux aussi afficher dans la console la valeur de value et le résultat de est_valide(value) pour comprendre ce qui se passe.

Téléverse le programme du chapitre 10 sur la carte client, et celui de ce chapitre sur la carte serveur, en vérifiant qu’ils partagent la même configuration radio (canal et groupe). Commence par envoyer des calculs simples composés uniquement de chiffres, d’opérateurs et de parenthèses : la carte serveur doit les exécuter et renvoyer le résultat comme avant.
Modifie ensuite le message côté client en ajoutant un caractère interdit (une lettre, un espace, un retour à la ligne, une instruction Python…) : cette fois, le serveur ne doit plus exécuter le calcul ni renvoyer de résultat. Compare le comportement avec celui du chapitre 11, où le code malveillant était exécuté.

Dans ce chapitre, nous avons franchi une étape importante : au lieu d’exécuter aveuglément tout ce qui est reçu, notre calculatrice commence à se méfier des messages de l’utilisateur. En écrivant une fonction est_valide qui vérifie caractère par caractère si le texte ressemble bien à un calcul et rien d’autre, nous avons vu qu’il est possible de bloquer les tentatives d’injection de code en amont, avant même l’exécution.
Notre micro:bit n’est toujours qu’une petite calculatrice à distance, mais elle illustre désormais un principe fondamental en informatique : ne jamais faire confiance aux données d’entrée sans les contrôler. Dans des systèmes réels (sites web, applications, services en ligne), ce principe est appliqué avec des règles et des outils plus complexes, mais l’idée reste la même que dans notre fonction est_valide.

Pour aller plus loin :
  • Se renseigner sur les recommandations de l’OWASP concernant la prévention des injections (SQL, scripts, etc.), qui reposent justement sur ce principe : ne jamais faire confiance aux données fournies par l’utilisateur sans les contrôler.

User licence