La saisie de texte sécurisée
Le langage C est un langage difficile. Il est nécessaire de connaître au moins un peu le fonctionnement de la mémoire, sans quoi on ne peut pas comprendre ce que l'on fait. Vous avez vu les pointeurs par exemple : c'est totalement impossible de les utiliser si on ne comprend pas comment ça marche derrière... ou alors on fait carrément n'importe quoi
Un des aspects les plus délicats du langage est la saisie de texte. Vous connaissez la fonction
scanf, que vous avez vue au début du cours. Vous vous dites : quoi de plus simple et de plus naturel ?
Eh bien figurez-vous que non, en fait, c'est tout sauf simple. J'ai volontairement simplifié mes explications au début du cours pour que tout le monde puisse suivre, mais du coup vous n'avez jamais vu à quel point le sujet est délicat et complexe.
Pourquoi ? Parce que la personne qui va utiliser votre programme est un humain, et que
tout humain qui se respecte fait des erreurs et peut avoir des comportements inattendus. Si vous lui demandez : "
Quel âge avez-vous ?", qu'est-ce qui vous garantit qu'il ne va pas vous répondre "
Je m'appelle François je vais bien merci !" ?
Le but de cette annexe est de vous faire découvrir les problèmes que l'on peut rencontrer en utilisant la fonction
scanf (problèmes de sécurité notamment) et de vous montrer une alternative beaucoup plus sûre avec la fonction
fgets.
Une parfaite connaissance du chapitre sur les
chaînes de caractères est nécessaire pour comprendre cette annexe !
La fonction
scanf(), que je vous ai présentée dès le début du cours de C, est une fonction à double tranchant :
- Elle est facile à utiliser quand on débute (c'est pour ça que je vous l'ai présentée)...
- ... mais son fonctionnement interne est complexe et elle peut même être dangereuse dans certains cas.
C'est un peu contradictoire n'est-ce pas ?

En fait, scanf a l'air facile à utiliser, mais elle ne l'est pas en pratique. Je vais vous montrer ses limites par 2 exemples concrets.
Entrer une chaîne de caractères avec des espaces
Supposons qu'on demande une chaîne de caractères à l'utilisateur, mais que celui-ci insère un espace dans sa chaîne :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13 | #include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char nom[20] = {0};
printf("Quel est votre nom ? ");
scanf("%s", nom);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
|
Code : Console | Quel est votre nom ? Jean Dupont
Ah ! Vous vous appelez donc Jean ! |
Pourquoi le "Dupont" a disparu ?
Parce que la fonction scanf s'arrête si elle tombe au cours de sa lecture sur un espace, une tabulation ou une entrée.
Vous ne pouvez donc pas récupérer la chaîne si celle-ci comporte un espace.
En fait, le mot "Dupont" se trouve toujours en mémoire, dans ce qu'on appelle le buffer. La prochaine fois qu'on appellera scanf, la fonction lira tout seule le mot "Dupont" qui était resté en attente dans la mémoire.
On peut utiliser la fonction scanf de telle sorte qu'elle lise les espaces, mais c'est assez compliqué. Si vous voulez apprendre à bien vous servir de scanf, je ne peux que vous recommander ce
tutoriel sur scanf de developpez.com. Attention, c'est assez difficile.
Entrer une chaîne de caractères trop longue
Il y a un autre problème, beaucoup plus grave encore : celle du
dépassement de mémoire.
Reprenons exactement le même code que tout à l'heure :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13 | #include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char nom[5] = {0};
printf("Quel est votre nom ? ");
scanf("%s", nom);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
|
Vous voyez que j'ai alloué 5 cases pour mon tableau de char "nom". Cela signifie qu'il y a la place d'écrire 4 caractères, le dernier étant toujours réservé au caractère de fin de chaîne \0.
Revoyez absolument le
cours sur les chaînes de caractères si vous avez oublié tout cela.
L'allocation mémoire qui a été faite est donc la suivante :
Que se passe-t-il si vous écrivez plus de caractères qu'il n'y a d'espace prévu pour les stocker ?
Code : Console | Quel est votre nom ? Patrice
Ah ! Vous vous appelez donc Patrice ! |
A priori, il ne s'est rien passé. Et pourtant, ce que vous voyez là est un véritable cauchemar de programmeur
On dit qu'on vient de faire un dépassement de mémoire, aussi appelé
buffer overflow en anglais.
Comme vous le voyez, on avait alloué 5 cases pour stocker le nom, mais il en fallait en fait 8. Qu'a fait la fonction scanf ?
Elle a continué à écrire à la suite en mémoire comme si de rien n'était !
Elle a écrit dans des zones mémoire qui n'étaient pas prévues pour cela.
Les caractères en trop ont "écrasé" d'autres informations en mémoire. C'est ce qu'on appelle le
buffer overflow :
En quoi cela est-il dangereux ?
Sans rentrer dans les détails, parce que c'est un peu compliqué et ce n'est pas le but de cette annexe, il faut savoir que si le programme ne contrôle pas ce genre de cas,
l'utilisateur peut écrire ce qu'il veut à la suite en mémoire.
En particulier, il peut insérer du code en mémoire et faire en sorte qu'il soit exécuté par le programme. C'est l'
attaque par buffer overflow, une attaque de hacker célèbre mais difficile à réaliser.
Le but de ce chapitre sera de sécuriser la saisie de nos données, en empêchant l'utilisateur de faire déborder et de provoquer un buffer overflow. Bien sûr, on pourrait allouer un très grand tableau (10 000 caractères), mais ça ne changerait rien au problème : une personne qui
veut faire dépasser de la mémoire n'aura qu'à envoyer plus de 10 000 caractères et son attaque marchera tout aussi bien.
Aussi bête que cela puisse paraître, tous les programmeurs n'ont pas toujours fait attention à cela. S'ils avaient fait les choses proprement depuis le début, il n'y aurait pas eu une bonne partie des "failles de sécurité" dont on entend parler encore aujourd'hui !
Il existe plusieurs fonctions standard en C qui permettent de récupérer une chaîne de texte. Hormis la fonction scanf (trop compliquée pour être étudiée ici), il existe :
- gets : une fonction qui lit toute une chaîne de caractères, mais très dangereuse car elle ne permet pas de contrôler les buffer overflow !
- fgets : l'équivalent de gets mais en version sécurisée, permettant de contrôler le nombre de caractères écrits en mémoire.
Vous l'aurez compris : bien que ce soit une fonction standard du C,
gets est très dangereuse. Tous les programmes qui l'utilisent sont susceptibles d'être victimes de buffer overflow.
Nous allons donc voir comment fonctionne
fgets et comment on peut l'utiliser en pratique dans nos programmes en remplacement de
scanf.
La fonction fgets
Le prototype de la fonction fgets, situé dans stdio.h, est le suivant :
Code : C1 | char *fgets( char *str, int num, FILE *stream );
|
Il est important de bien comprendre ce prototype. Les paramètres sont :
- str : un pointeur vers un tableau alloué en mémoire où la fonction va pouvoir écrire le texte entré par l'utilisateur.
- num : la taille du tableau str envoyé en premier paramètre.
Notez que si vous avez alloué un tableau de 10 char, fgets lira 9 caractères au maximum (il réserve toujours un caractère d'espace pour pouvoir écrire l'\0 de fin de chaîne).
- stream : un pointeur sur le fichier à lire. Dans notre cas, le "fichier à lire" est l'entrée standard, c'est-à-dire le clavier. Pour demander à lire l'entrée standard, on enverra le pointeur "stdin", qui est automatiquement défini dans les headers de la bibliothèque standard du C pour représenter le clavier.
Toutefois, il est aussi possible d'utiliser fgets pour lire des fichiers, comme on a pu le voir dans le chapitre sur les fichiers.
La fonction
fgets retourne le même pointeur que
str si la fonction s'est déroulée sans erreur, ou NULL s'il y a eu une erreur. Il suffit donc de tester si la fonction a renvoyé NULL pour savoir s'il y a eu une erreur.
Testons !
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13 | #include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char nom[10];
printf("Quel est votre nom ? ");
fgets(nom, 10, stdin);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
|
Code : Console | Quel est votre nom ? Mateo
Ah ! Vous vous appelez donc Mateo
! |
Ça fonctionne très bien, à un détail près : quand vous tapez "Entrée", fgets conserve l'\n correspondant à l'appui sur la touche "Entrée". Cela se voit dans la console car il y a un saut à la ligne après "Mateo" dans mon exemple.
On ne peut rien faire pour empêcher fgets d'écrire le caractère \n, la fonction est faite comme ça. En revanche, rien ne nous interdit de créer notre propre fonction de saisie qui va appeler fgets et supprimer automatiquement à chaque fois les \n !
Créer sa propre fonction de saisie utilisant fgets
Il n'est pas très difficile de créer sa propre petite fonction de saisie qui va faire quelques corrections à chaque fois pour nous.
Nous appellerons cette fonction
lire. Elle renverra 1 si tout s'est bien passé, 0 s'il y a eu une erreur.
Eliminer le saut de ligne \n
La fonction
lire va appeler fgets et, si tout s'est bien passé, elle va rechercher le caractère \n à l'aide de la fonction strchr que vous devriez déjà connaître. Si un \n est trouvé, elle le remplace par un \0 (fin de chaîne) pour éviter de conserver une "Entrée".
Voici le code, commenté pas à pas :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | #include <stdio.h>
#include <stdlib.h>
#include <string.h> // Penser à inclure string.h pour strchr()
int lire(char *chaine, int longueur)
{
char *positionEntree = NULL;
// On lit le texte saisi au clavier
if (fgets(chaine, longueur, stdin) != NULL) // Si la saisie se fait sans erreur
{
positionEntree = strchr(chaine, '\n'); // On recherche l'"Entrée"
if (positionEntree != NULL) // Si on a trouvé le retour à la ligne
{
*positionEntree = '\0'; // On remplace ce caractère par \0
}
return 1; // On renvoie 1 si la fonction s'est déroulée sans erreur
}
else
{
return 0; // On renvoie 0 s'il y a eu une erreur
}
}
|
Vous noterez que je me permets d'appeler la fonction fgets directement dans un if. Ça m'évite d'avoir à récupérer la valeur de fgets dans un pointeur juste pour tester si celui-ci est NULL ou pas.
A partir du premier if, je sais si fgets s'est bien déroulée ou s'il y a eu un problème (l'utilisateur a rentré plus de caractères qu'il n'était autorisé).
Si tout s'est bien passé, je peux alors partir à la recherche du \n avec strchr et remplacer cet \n par un \0.
Ce schéma montre que la chaîne écrite par fgets était "
Mateo\n\0". Nous avons remplacé le \n par un \0, ce qui a donné au final : "
Mateo\0\0".
Ce n'est pas grave d'avoir deux \0 d'affilée. L'ordinateur s'arrête au premier \0 qu'il rencontre et considère que la chaîne de caractères s'arrête là.
Le résultat ? Ben, ça marche
Code : C 1
2
3
4
5
6
7
8
9
10 | int main(int argc, char *argv[])
{
char nom[10];
printf("Quel est votre nom ? ");
lire(nom, 10);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
|
Code : Console | Quel est votre nom ? Mateo
Ah ! Vous vous appelez donc Mateo ! |
Vider le buffer
Nous ne sommes pas encore au bout de nos ennuis.
Nous n'avons pas étudié ce qui se passait si l'utilisateur tentait de mettre plus de caractères qu'il n'y avait de place !
Code : Console | Quel est votre nom ? Jean Edouard Albert 1er
Ah ! Vous vous appelez donc Jean Edou ! |
La fonction fgets étant sécurisée, elle s'est arrêtée de lire au bout du 9ème caractère, car nous avions alloué un tableau de 10 char (il ne faut pas oublier le caractère de fin de chaîne \0 qui occupe la 10ème position).
Le problème, c'est que le reste de la chaîne qui n'a pas pu être lu, à savoir "ard Alber 1er", n'a pas disparu ! Il est toujours dans le
buffer.
Le buffer est une sorte de zone mémoire qui reçoit directement l'entrée clavier et qui sert d'intermédiaire entre le clavier et votre tableau de stockage. En C, on dispose d'un pointeur vers le buffer, c'est
stdin dont je vous parlais plus haut !
Je crois qu'un petit schéma ne sera pas de refus pour mettre les idées au clair :
Lorsque l'utilisateur tape du texte au clavier, le système d'exploitation (Windows par exemple) copie directement le texte tapé dans le buffer stdin. Ce buffer est là pour recevoir temporairement l'entrée du clavier.
Le rôle de la fonction
fgets est justement d'extraire du buffer les caractères qui s'y trouvent et de les copier dans la zone mémoire que vous lui indiquez (votre tableau
chaine).
Après avoir effectué son travail de copie,
fgets enlève du buffer tout ce qu'elle a pu copier.
Si tout s'est bien passé,
fgets a donc pu copier tout le buffer dans votre chaîne, et le buffer est donc vide à la fin de l'exécution de la fonction.
Mais si l'utilisateur rentre beaucoup de caractères, et que la fonction fgets ne peut copier qu'une partie d'entre eux (parce que vous avez alloué un tableau de 10 char seulement), seuls les caractères lus seront supprimés du buffer. Tous ceux qui n'auront pas été lus y resteront !
Testons avec une longue chaîne :
Code : C 1
2
3
4
5
6
7
8
9
10 | int main(int argc, char *argv[])
{
char nom[10];
printf("Quel est votre nom ? ");
lire(nom, 10);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
|
Code : Console | Quel est votre nom ? Jean Edouard Albert 1er
Ah ! Vous vous appelez donc Jean Edou ! |
La fonction fgets n'a pu copier que les 9 premiers caractères comme prévu. Le problème, c'est que les autres se trouvent toujours dans le buffer !
Cela signifie que si vous faites un autre fgets après, celui-ci va aller récupérer ce qui était resté en mémoire dans le buffer !
Testez ce code :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12 | int main(int argc, char *argv[])
{
char nom[10];
printf("Quel est votre nom ? ");
lire(nom, 10);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
lire(nom, 10);
printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
return 0;
}
|
Nous appelons deux fois la fonction lire. Pourtant, vous allez voir qu'on ne vous laisse pas taper 2 fois votre nom : en effet, la fonction fgets ne demande pas à l'utilisateur de taper du texte la seconde fois car elle trouve du texte à récupérer dans le buffer !
Code : Console | Quel est votre nom ? Jean Edouard Albert 1er
Ah ! Vous vous appelez donc Jean Edou !
Ah ! Vous vous appelez donc ard Alber ! |
Si l'utilisateur tape trop de caractères, la fonction
fgets nous protège contre le débordement de mémoire, mais il reste toujours des traces du texte en trop dans le buffer. Il faut
vider le buffer.
On va donc améliorer notre petite fonction
lire et appeler si besoin est une sous-fonction
viderBuffer pour faire en sorte que le buffer soit vidé si on a rentré trop de caractères :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | void viderBuffer()
{
int c = 0;
while (c != '\n' || c != EOF)
{
c = getchar();
}
}
int lire(char *chaine, int longueur)
{
char *positionEntree = NULL;
if (fgets(chaine, longueur, stdin) != NULL)
{
positionEntree = strchr(chaine, '\n');
if (positionEntree != NULL)
{
*positionEntree = '\0';
}
else
{
viderBuffer();
}
return 1;
}
else
{
viderBuffer();
return 0;
}
}
|
La fonction lire appelle viderBuffer dans 2 cas :
- 1er cas : si la chaîne était trop longue (on le sait parce qu'on n'a pas trouvé de caractère \n dans la chaîne copiée)
- 2ème cas : il y a eu une erreur (peu importe laquelle) et il faut vider là aussi le buffer par sécurité pour qu'il n'y ait plus rien.
La fonction viderBuffer est courte mais dense. Elle lit dans le buffer caractère par caractère grâce à getchar. Cette fonction renvoie un int (et non un char, allez savoir pourquoi, peu importe).
On se contente de récupérer cet int dans la variable temporaire "c". On boucle tant qu'on n'a pas récupéré le caractère "\n" ou le symbole EOF (fin de fichier), qui signifient tous les deux "vous êtes arrivé à la fin du buffer". On s'arrête donc de boucler dès que l'on tombe sur l'un de ces 2 caractères.
Et voilà le travail !

C'est un peu compliqué au premier abord, assez technique, mais croyez-moi : ça a du sens et ça peut se comprendre ! Je vous laisse étudier tranquillement ces fonctions, en vous aidant de mes schémas vous devriez finir par comprendre.
Notre fonction
lire est maintenant efficace et robuste, mais elle ne sait lire que du texte. Vous devez vous demander : "mais comment fait-on pour récupérer un nombre" ?
En fait,
lire est une fonction de base. Avec
fgets, vous ne pouvez récupérer que du texte, mais il existe d'autres fonctions qui permettent de convertir ensuite un texte en nombre.
strtol : convertir une chaîne en long
Le prototype de la fonction strtol est un peu particulier :
Code : C1 | long strtol( const char *start, char **end, int base );
|
La fonction lit la chaîne de caractères que vous lui envoyez (
start) et essaie de la convertir en long en utilisant la
base indiquée (généralement on travaille en base 10 car on utilise 10 chiffres différents de 0 à 9, donc vous mettrez 10). Elle retourne le nombre qu'elle a réussi à lire.
Quant au pointeur de pointeur
end, la fonction s'en sert pour renvoyer la position du premier caractère qu'elle a lu et qui n'était pas un nombre. On ne s'en servira pas, donc on peut lui envoyer NULL pour lui faire comprendre qu'on ne veut rien récupérer
La chaîne doit commencer par un nombre, tout le reste est ignoré. Elle peut être précédée d'espaces.
Quelques exemples d'utilisation pour bien comprendre le principe :
Code : C1
2
3
4
5
6
7
8 | long i;
i = strtol( "148", NULL, 10 ); // i = 148
i = strtol( "148.215", NULL, 10 ); // i = 148
i = strtol( " 148.215", NULL, 10 ); // i = 148
i = strtol( " 148+34", NULL, 10 ); // i = 148
i = strtol( " 148 feuilles mortes", NULL, 10 ); // i = 148
i = strtol( " Il y a 148 feuilles mortes", NULL, 10 ); // i = 0 (erreur : la chaîne ne commence pas par un nombre)
|
Toutes les chaînes qui commencent par un nombre (ou éventuellement par des espaces suivis d'un nombre) seront converties en long jusqu'à la première lettre ou au premier caractère invalide (. + etc).
La dernière chaîne, ne commençant pas par un nombre, ne peut pas être convertie. La fonction
strtol renverra donc 0.
On peut créer une fonction lireLong qui va appeler notre première fonction lire (qui lit du texte) et ensuite convertir le texte saisi en nombre :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | long lireLong()
{
char nombreTexte[100] = {0}; // 100 cases devraient suffire
if (lire(nombreTexte, 100))
{
// Si lecture du texte ok, convertir le nombre en long et le retourner
return strtol(nombreTexte, NULL, 10);
}
else
{
// Si problème de lecture, renvoyer 0
return 0;
}
}
|
Vous pouvez tester dans un main très simple :
Code : C 1
2
3
4
5
6
7
8
9
10 | int main(int argc, char *argv[])
{
long age = 0;
printf("Quel est votre age ? ");
age = lireLong();
printf("Ah ! Vous avez donc %ld ans !\n\n", age);
return 0;
}
|
Code : Console | Quel est votre age ? 18
Ah ! Vous avez donc 18 ans ! |
strtod : convertir une chaîne en double
La fonction
strtod est identique à
strtol, à la différence près qu'elle essaie de lire un nombre décimal et renvoie un double :
Code : C1 | double strtod( const char *start, char **end );
|
Vous noterez que le troisième paramètre
base a disparu ici, mais on se tape toujours le pointeur de pointeur
end qui ne nous sert à rien.
Contrairement à strtol, cette fois la fonction prend en compte le "point" décimal. Attention en revanche : elle ne connaît pas la virgule (c'est pas un truc que les anglais connaissent

).
Exercice : écrire la fonction
lireDouble. Vous ne devriez avoir aucun mal à le faire, c'est exactement comme
lireLong à part que cette fois on appelle
strtod et on retourne un double
Vous devriez pouvoir alors faire ceci dans la console :
Code : Console | Combien pesez-vous ? 67.4
Ah ! Vous pesez donc 67.400000 kg ! |
Exercice (suite) : modifiez votre fonction
lireDouble pour qu'elle accepte aussi le symbole virgule comme séparateur décimal. La technique est simple : remplacez la virgule par un point dans la chaîne de texte lue (grâce à la fonction de recherche
strchr), puis envoyez la chaîne modifiée à
strtod.
Cette annexe, bien qu'un peu délicate, vous aura appris à récupérer le texte saisi par l'utilisateur grâce à des méthodes plus sûres et bien plus robustes.
Comme nous avons réalisé plusieurs fonctions, je vous conseille de les mettre dans un fichier lecture.c et de créer le fichier .h correspondant avec les prototypes.
Je vous conseille d'utiliser de préférence ces fonctions dans tous vos futurs projets plutôt que la fonction
scanf, pour des raisons de sécurité notamment (il est facile de bousiller sa mémoire si on n'y prend pas garde, on l'a vu).
Oh et puisqu'on y est, je vous propose de télécharger carrément les
fichiers lecture.c et lecture.h que j'ai créés avec vous tout au long du chapitre.
Non non ne me remerciez pas, c'est tout naturel
Informations sur le tutoriel
Retour en haut
Créé : Le 29/07/2005 à 00:29:36
Modifié : Le 27/06/2009 à 00:44:36
Avancement : 100%
Licence : Copie non autorisée
41 commentaires