Éviter les failles de sécurité dès le développement d'une application - 6 : les scripts CGI

ArticleCategory:

Software Development

AuthorImage:

[image of the authors]

TranslationInfo:

Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier

AboutTheAuthor:

Christophe Blaess est un ingénieur indépendant dans le domaine de l'aéronautique Passionné par Linux, il effectue l'essentiel de son travail sur ce système, et assure la coordination des traductions des pages de manuel publiées par le Linux Documentation Project.

Christophe Grenier est étudiant en 5ème année à l'ESIEA, où il est également administrateur système. La sécurité informatique est l'une de ses passions.

Frédéric Raynal utilise Linux depuis des années parce qu'il ne pollue pas, qu'il est garanti sans hormones, OGM ou farines animales... il ne réclame que de la sueur et de l'astuce.

Abstract

Obtenir un fichier, exécuter un programme via un script Perl mal programmé ... "There's More Than One Way To Do It!"

Les articles précédents de la série :

ArticleIllustration:[illustration]

[article illustration]

ArticleBody:[The real article: put the text and html-codes here]

Serveur web, URI et problèmes de configuration

Introduction (trop) rapide sur le fonctionnement d'un serveur web et la construction d'une URL

Lorsqu'un client envoie une requête pour obtenir un fichier HTML, le serveur lui retourne la page demandée (ou un message d'erreur). Le navigateur interprète alors le code HTML pour le mettre en forme et l'afficher. Par exemple, avec l'URL (Uniform Request Locator) www.linuxdoc.org/HOWTO/HOWTO-INDEX/howtos.html, le client se connecte au serveur www.linuxdoc.org et lui demande la page /HOWTO/HOWTO-INDEX/howtos.html (appelée URI - Uniform Resource Identifiers), le dialogue ayant lieu en employant le protocole HTTP. Si cette page existe, le serveur renvoie le contenu du fichier demandé. Dans ce modèle, qualifié de statique, si le fichier est présent sur le serveur, il est transmis tel quel au client, sinon un message d'erreur (le fameux 404 - Not Found) est renvoyé.

Malheureusement, ce schéma ne permet pas d'interactions avec un utilisateur, ce qui rend impossible les opérations comme le e-commerce, la e-réservation de ses vacances ou encore le e-n'importe quoi.

Heureusement, il existe des solutions pour générer dynamiquement des pages HTML. Les scripts CGI (Common Gateway Interface) sont l'une d'elles. A la différence du cas précédent, les URLs pour y accéder sont construites légèrement différemment :

  http://<serveur><chemin vers le script>[?[param_1=val_1][...][¶m_n=val_n]] 
La liste d'arguments est stockée dans la variable d'environnement QUERY_STRING. Un script CGI, dans ce contexte, n'est rien d'autre qu'un fichier exécutable. Il utilise l'entrée standard stdin pour récupérer les arguments qui lui sont passés. Après exécution de son code, il affiche son résultat sur la sortie standard stdout, qui est alors redirigée vers le client web. Pratiquement n'importe quel langage de programmation convient pour écrire un script CGI (programme C compilé, Perl, shell-scripts...).

Par exemple, recherchons ce que les HOWTOs sur www.linuxdoc.org savent de ssh :

http://www.linuxdoc.org/cgi-bin/ldpsrch.cgi?
  svr=http%3A%2F%2Fwww.linuxdoc.org&srch=ssh&db=1&scope=0&rpt=20
En fait, ceci est beaucoup plus simple qu'il n'y paraît. Décomposons cette URL :

Ici, avouons tout de suite, au risque de mettre fin à une légende, que nous ne sommes pas devins. Souvent, les noms des arguments et leurs valeurs sont suffisament explicites pour comprendre leurs significations. En outre, le contenu de la page affichant les résultats est pour le moins révélateur.

Vous l'aurez compris, ce qui fait l'intérêt des scripts CGI est la possibilité laissée à un utilisateur de passer des arguments... mais c'est également ce qui fait qu'un script mal écrit ouvre une faille de sécurité.

Vous avez également sans doute déjà remarqué les caractères étranges employés par votre navigateur favori ou présents dans la requête précédente. Ces caractères suivent le format iso_8859-1 (la commande man iso_8859_1 vous donnera tous les détails). Le tableau 1 donne la signification de certains codes. Signalons que certains serveurs IIS4.0 et IIS5.0 sont sensibles à une vulnérabilité fondée sur l'emploi de tels caractères (appelée bug unicode).

Configuration d'Apache avec "SSI Server Side Include"

Server Side Include est une fonction intégrée aux serveurs web. Elle permet d'intégrer des directives dans les pages web, soit pour inclure un fichier tel quel, soit pour exécuter une commande (shell ou script CGI).

Dans le fichier de configuration httpd.conf d'Apache, l'instruction "AddHandler server-parsed .shtml" active ce mécanisme. Souvent, afin d'éviter le distinguo entre .html et .shtml, on y ajoute également l'extension .html. Évidemment, cela ralentit un peu le serveur... Cette possibilité est contrôlée au niveau des répertoires par les directives  :

Dans le script guestbook.cgi ci-joint, le texte fourni par l'utilisateur est directement inclus dans un fichier HTML, sans conversion des caractères '<' et ' >' en code html &lt; et &gt;. Il suffit alors à une personne curieuse de soumettre une des instructions suivantes :

Avec la première, guestbook.cgi?email=pappy&texte=%3c%21--%23printenv%20--%3e, quelques informations sur le système sont révélées :
DOCUMENT_ROOT=/home/web/sites/www8080
HTTP_ACCEPT=image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*
HTTP_ACCEPT_CHARSET=iso-8859-1,*,utf-8
HTTP_ACCEPT_ENCODING=gzip
HTTP_ACCEPT_LANGUAGE=en, fr
HTTP_CONNECTION=Keep-Alive
HTTP_HOST=www.esiea.fr:8080
HTTP_PRAGMA=no-cache
HTTP_REFERER=http://www.esiea.fr:8080/~grenier/cgi/guestbook.cgi?
 email=&texte=%3C%21--%23include+file%3D%22guestbook.cgi%22--%3E
HTTP_USER_AGENT=Mozilla/4.76 [fr] (X11; U; Linux 2.2.16 i686)
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/X11R6/bin
REMOTE_ADDR=194.57.201.103
REMOTE_HOST=nef.esiea.fr
REMOTE_PORT=3672
SCRIPT_FILENAME=/mnt/c/nef/neftiles/grenier/public_html/cgi/guestbook.html
SERVER_ADDR=194.57.201.103
SERVER_ADMIN=master8080@nef.esiea.fr
SERVER_NAME=www.esiea.fr
SERVER_PORT=8080
SERVER_SIGNATURE=<ADDRESS>Apache/1.3.14 Server at www.esiea.fr Port 8080</ADDRESS>

SERVER_SOFTWARE=Apache/1.3.14 (Unix)  (Red-Hat/Linux) PHP/3.0.18
GATEWAY_INTERFACE=CGI/1.1
SERVER_PROTOCOL=HTTP/1.0
REQUEST_METHOD=GET
QUERY_STRING=
REQUEST_URI=/~grenier/cgi/guestbook.html
SCRIPT_NAME=/~grenier/cgi/guestbook.html
DATE_LOCAL=Tuesday, 27-Feb-2001 15:33:56 CET
DATE_GMT=Tuesday, 27-Feb-2001 14:33:56 GMT
LAST_MODIFIED=Tuesday, 27-Feb-2001 15:28:05 CET
DOCUMENT_URI=/~grenier/cgi/guestbook.shtml
DOCUMENT_PATH_INFO=
USER_NAME=grenier
DOCUMENT_NAME=guestbook.shtml

Quant à la directive exec, elle donne pratiquement l'équivalent d'un shell :


guestbook.cgi?email=ppy&texte=%3c%21--%23exec%20cmd="cat%20/etc/passwd"%20--%3e
.

Pas la peine d'essayer "<!--#include file="/etc/passwd"-->", le chemin est relatif au répertoire où se trouve le fichier HTML et il ne peut contenir de "..". Le fichier error_log d'Apache contient alors un message signalant une tentative d'accès à un fichier interdit. L'utilisateur voit dans la page html le message [an error occurred while processing this directive].

Il est assez rare d'avoir besoin des SSI. Le plus sage est de les désactiver de son serveur. Dans le cas présent, le problème vient de la combinaison de l'application guestbook boguée et de SSI.

Scripts Perl

Dans cette partie, nous présentons des failles de sécurité liées à l'emploi de Perl pour écrire des scripts CGI. Afin de tenter de conserver une certaine lisibilité, nous ne fournissons pas le code complet des exemples, seulement les extraits nécessaires pour comprendre où se situe le problème.

Tous nos scripts sont construits sur le modèle suivant :

#!/usr/bin/perl -wT
BEGIN { $ENV{PATH} = '/usr/bin:/bin' }
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};   # Make %ENV safer =:-)
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD>";
print "<TITLE>Remote Command</TITLE></HEAD>\n";
&ReadParse(\%input);
# utilisez plutôt $input de cette manière :
# print "<p>$input{filename}</p>\n";
# #################################### #
# Début du code descriptif du problème #
# #################################### #



# ################################## #
# Fin du code descriptif du problème #
# ################################## #

form:
print "<form action=\"$ENV{'SCRIPT_NAME'}\">\n";
print "<input type=texte name=filename>\n </form>\n";
print "</BODY>\n";
print "</HTML>\n";
exit(0);

# le premier argument doit être une référence à un hachage. 
# Le hachage doit être rempli de données.
sub ReadParse {
  local (*in) = @_ if @_;
  my ($i, $key, $val);
  my $in_first;
  my @in_second;

  # Read in text
   if ($ENV{'REQUEST_METHOD'} eq "GET") {
    $in_first = $ENV{'QUERY_STRING'};
  } elsif ($ENV{'REQUEST_METHOD'} eq "POST") {
    read(STDIN,$in_first,$ENV{'CONTENT_LENGTH'});
    }else{
    die "ERROR: unknown request method\n";
  }

  @in_second = split(/&/,$in_first);

  foreach $i (0 .. $#in_second) {
    # Convert plus's to spaces
    $in_second[$i] =~ s/\+/ /g;

    # Split into key and value.  
    ($key, $val) = split(/=/,$in_second[$i],2); 

    # Convert %XX from hex numbers to alphanumeric
    $key =~ s/%(..)/pack("c",hex($1))/ge;
    $val =~ s/%(..)/pack("c",hex($1))/ge;

    # Associate key and value
    $in{$key} .= "\0" if (defined($in{$key}));
    $in{$key} .= $val;

  }

  return length($#in_second); 
}

Nous reviendrons ultérieurement sur les arguments passés à Perl (-wT). Nous commençons par nettoyer nos variables d'environnement $ENV et $PATH, puis nous envoyons l'entête du fichier HTML. La fonction ReadParse() permet de récupérer les arguments passés au script. Des modules permettent de faire ceci bien plus souplement, mais pour vous éviter de devoir les installer si ce n'est déjà fait sur votre serveur web, cette fonction suffit amplement. Ensuite, les exemples présentés ci-dessous prennent place. Enfin, nous concluons notre fichier HTML.

L'octet nul

Perl considère tous les caractères de manières identiques, ce qui n'est pas le cas des fonctions C par exemple. Le caractère nul de fin de chaîne est, pour Perl, un caractère comme les autres. Et alors ?

Ajoutons le code suivant à notre script pour obtenir showhtml.cgi :

  # showhtml.cgi
  my $filename= $input{filename}.".html";
  print "<BODY>File : $filename<BR>";
  if (-e $filename) {
      open(FILE,"$filename") || goto form;
      print <FILE>;
  }

La fonction ReadParse() récupère l'unique argument transmis : le nom du fichier à afficher. Pour éviter qu'un petit comique essaye de lire autre chose que les fichiers HTML, nous ajoutons l'extension ".html" à la fin du nom du fichier. Mais rappelez-vous que le caractère nul est un caractère comme les autres ...

Ainsi, lorsque notre requête est showhtml.cgi?filename=%2Fetc%2Fpasswd%00, le fichier s'appelle alors my $filename = "/etc/passwd\0.html" et nos yeux ébahis contemplent un fichier qui n'est pas du HTML.

Que se passe-t-il ? La commande strace nous montre bien le cheminement suivi lors de l'ouverture d'un fichier en Perl :

  /tmp >>cat >open.pl << EOF
  > #!/usr/bin/perl
  > open(FILE, "/etc/passwd\0.html");
  > EOF
  /tmp >>chmod 0700 open.pl 
  /tmp >>strace ./open.pl 2>&1 | grep open
  execve("./open.pl", ["./open.pl"], [/* 24 vars */]) = 0
  ...
  open("./open.pl", O_RDONLY)             = 3
  read(3, "#!/usr/bin/perl\n\nopen(FILE, \"/et"..., 4096) = 51
  open("/etc/passwd", O_RDONLY)           = 3


Le dernier open() affiché par strace correspond à l'appel-système, écrit en C. On constate que l'extension .html a disparu, ce qui provoque bien l'ouverture du fichier protégé.

Ce problème se règle avec une simple expression régulière, chose assez simple en Perl :

s/\0//g

L'utilisation du pipe

Voici maintenant le cas d'un script sans aucune protection qui affiche le fichier de votre choix sous une certaine arborescence :

#pipe1.cgi

my $filename= "/home/httpd/".$input{filename};
print "<BODY>File : $filename<BR>";
open(FILE,"$filename") || goto form;
print <FILE>;

Ne vous moquez pas de cet exemple ! Un fichier template est un modèle comportant des variables ($nom_utilisateur, $nom_societe...) qui sont définis dans le programme Perl. J'ai trouvé un bogue similaire dans un script où le fichier template était passé en argument.

La première erreur est évidente :

pipe1.cgi?filename=..%2F..%2F..%2Fetc%2Fpasswd"
Il suffit de remonter par soi-même dans les répertoires pour accéder n'importe où. Mais il existe une possibilité bien plus intéressante : exécuter la commande de notre choix. En effet, en Perl, la commande open(FILE, "/bin/ls") ouvre le fichier binaire "/bin/ls"... mais open(FILE, "/bin/ls |") exécute la commande spécifiée. L'ajout d'un simple tube '| change le comportement du open().

Un autre problème vient de ce qu'ici, l'existence du fichier n'est pas testée, ce qui permet non seulement d'exécuter n'importe quelle commande, mais aussi de lui passer les arguments de son choix : pipe1.cgi?filename=..%2F..%2F..%2Fbin%2Fcat%20%2fetc%2fpasswd%20| affiche encore le contenu du fichier de mots de passe.

Tester l'existence du fichier à ouvrir laisse déjà moins de liberté :

#pipe2.cgi

my $filename= "/home/httpd/".$input{filename};
print "<BODY>File : $filename<BR>";
if (-e $filename) {
  open(FILE,"$filename") || goto form;
  print <FILE>
} else {
  print "-e failed: no file\n";
}
L'exemple précédent ne fonctionne plus. En effet, le test d'existence "-e" échoue car il ne trouve pas le fichier
"../../../bin/cat /etc/passwd |".

Essayons maintenant la commande /bin/ls. Le comportement sera le même qu'auparavant. En effet, si nous cherchons, par exemple, à lister le contenu du répertoire /etc, le "-e" teste alors l'existence du fichier "../../../bin/ls /etc |, mais il n'existe pas plus. Tant que nous ne fournirons pas le nom d'un fichier "fantôme", nous ne pourrons rien tirer d'intéressant :(

Tout n'est cependant pas perdu, même si le résultat est moins intéressant. Le fichier /bin/ls existe bien lui (enfin, sur la plupart des systèmes), mais si le open() est appelé juste avec ce nom de fichier, la commande ne sera pas exécutée mais le binaire sera affiché. Il faut donc trouver une solution pour placer un tube '|' à la fin du nom, sans que ce '|' ne soit utilisé dans la vérification provoquée par le "-e". Nous connaissons déjà la solution : l'octet nul. Si nous transmettons "../../../bin/ls\0|" comme nom, le test d'existence réussit car il ne portera que sur "../../../bin/ls" mais le open() verra bien le pipe et exécutera la commande. Ainsi, l'URI fournit le contenu du répertoire courant :

pipe2.cgi?filename=..%2F..%2F..%2Fbin%2Fls%00|

Retour à la ligne

Le script finger.cgi exécute l'instruction finger sur notre machine :

#finger.cgi

print "<BODY>";
$login = $input{'login'};
$login =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g;
print "Login $login<BR>\n";
print "Finger<BR>\n";
$CMD= "/usr/bin/finger $login|";
open(FILE,"$CMD") || goto form;
print <FILE>

Ce script prend (enfin) une mesure utile : il protège certains caractères étranges pour éviter qu'ils ne soient interprétés par un shell. On dit alors que ces caractères sont échappés. Ceci consiste simplement à placer un '\' devant afin d'en empêcher l'interprétation. Ainsi, le point-virgule est transformé en "\; par l'expression régulière. Mais la liste ne contient pas tous les caractères importants. Il manque, entre autre, le retour à la ligne '\n.

Sur la ligne de commande de votre shell favori, vous validez votre instruction en tapant sur les touches RETURN ou ENTER, ce qui provoque l'envoi d'un caractère '\n'. En Perl, le même mécanisme est possible. Nous avons déjà vu que l'instruction open() nous laissait exécuter une commande sous réserve de terminer la ligne par un tube '|'.

Pour simuler ce comportement, il nous suffit d'ajouter, après le login transmis à la commande finger, un retour-chariot puis l'instruction de notre choix :

finger.cgi?login=kmaster%0Acat%20/etc/passwd

Dans le même ordre d'idées, d'autres caractères sont également intéressants pour exécuter successivement plusieurs instructions :

Ils ne fonctionnent pas ici car ils sont pris en charge dans l'expression régulière qui les protège. Mais nous allons justement voir un moyen de nous en sortir quand même.

Backslash et point-virgule

Le script finger.cgi précédent prend des mesures pour éviter de se voir passer des caractères étranges. Ainsi, l'URI <finger.cgi?login=kmaster;cat%20/etc/passwd ne fonctionne pas car le point-virgule est échappé. Cependant, il est un caractère qui n'est pas très souvent protégé : le backslash '\'.

Par exemple, prenons le cas d'un script qui veut interdire de remonter dans une arborescence en utilisant le répertoire "..", l'expression régulière s/\.\.//g efface les "..", mais ça ne sert à rien car les shells supportent parfaitement l'amoncellement de plusieurs '/' (essayez cat ///etc//////passwd pour vous en convaincre).

Par exemple, dans le script pipe2.cgi ci-dessus, la variable $filename est initialisée à partir du préfixe "/home/httpd/". L'utilisation de l'expression régulière précédente pourrait sembler efficace pour empêcher de remonter dans les répertoires. Certes, cette expression protège de "..", mais que se passe-t-il si on ruse un peu en protégeant nous-mêmes le caractère '.' ? En effet, l'expression régulière ne correspond pas si le nom du fichier est .\./.\./etc/passwd. Signalons que ceci fonctionne très bien avec system() (ou les guillemets inverses ` ... `), mais le open() ou le "-e" échoue.

Revenons donc maintenant au script finger.cgi. Si on prend le cas du point-virgule, l'URI finger.cgi?login=kmaster;cat%20/etc/passwd ne nous donne pas le résultat escompté car le point-virgule est échappé par l'expression régulière. Ceci signifie que le shell reçoit l'instruction :

/usr/bin/finger kmaster\;cat /etc/passwd
Les erreurs suivantes sont inscrites dans les logs du serveur web :
finger: kmaster;cat: no such user.
finger: /etc/passwd: no such user.
Ces messages sont identiques à ceux obtenus en tapant directement cette ligne dans un shell. Le problème vient de ce que la protection mise sur le ';' fait apparaître ce caractère comme appartenant au nom "kmaster;cat".

Or, nous cherchons ici à séparer les deux instructions, celle du script et celle qui nous intéresse. Il faut donc protéger nous-mêmes le ';' : <A HREF="finger.cgi?login=kmaster\;cat%20/etc/passwd"> finger.cgi?login=kmaster\;cat%20/etc/passwd</A>. La chaîne "\; sera alors transformée par le script en "\\;", qui sera ensuite transmise au shell. Celui-ci voit donc :

/usr/bin/finger kmaster\\;cat /etc/passwd
Le shell décompose ceci en deux instructions :
  1. /usr/bin/finger kmaster\ qui a de forte chance d'échouer ... mais ça n'est pas notre problème ;-)
  2. cat /etc/passwd qui affiche le fichier de mots de passe.
La solution pour se protéger est simple : le backslash '\' doit également être échappé.

Utilisation du caractère " non protégé

Parfois, le paramètre est "protégé" à l'aide de guillemets. Nous avons légèrement modifié le script finger.cgi précédent pour protéger ainsi la variable $login.

Toutefois, si les guillemets ne sont pas échappés, c'est comme si rien n'était fait. En effet, il suffit simplement d'en ajouter dans sa propre requête. Ainsi, le premier guillemet transmis ferme celui (ouvrant) du script. Ensuite, on place la commande de notre choix, puis un second guillemet qui ouvre le dernier guillemet (fermant) du script.

Le script finger2.cgi illustre ceci :

#finger2.cgi

print "<BODY>";
$login = $input{'login'};
$login =~ s/\0//g;
$login =~ s/([<>\*\|`&\$!#\(\)\[\]\{\}:'\n])/\\$1/g;
print "Login $login<BR>\n";
print "Finger<BR>\n";
#Nouvelle super protection (in)efficace
$CMD= "/usr/bin/finger \"$login\"|";  
open(FILE,"$CMD") || goto form;
while(<FILE>) {
  print;
}

L'URI qui permet alors d'exécuter notre commande devient :

finger2.cgi?login=kmaster%22%3Bcat%20%2Fetc%2Fpasswd%3B%22
Le shell reçoit alors la commande /usr/bin/finger "$login";cat /etc/passwd"" où les guillemets ne sont plus un problème.

Il est donc important, si vous voulez protéger vos paramètres avec des guillemets, de les échapper, tout comme le point-virgule ou le backslash que nous avons déjà abordés.

Écrire en Perl

Options de warnings et corruption

Lorsque vous programmez en Perl, utilisez l'option w ou "use warnings;" (Perl 5.6.0 et ultérieurs), elle vous prévient de certains problèmes potentiels, comme des variables non initialisées ou des expressions/fonctions obsolètes.

L'option T ( taint mode de to taint, corrompre en Anglais) offre un niveau de sécurité supérieur. Ce mode déclenche plusieurs tests. Le plus intéressant concerne une éventuelle corruption des variables. Celles-ci sont soit propres, soit corrompues. Toutes les données qui proviennent de l'extérieur du programme sont considérées comme corrompues tant qu'elles n'ont pas été nettoyées. De même, toutes les variables issues de variables corrompues sont, elles aussi, dans cet état. Une telle variable ne peut alors affecter quoi que ce soit en dehors du programme.

En mode corruption, les arguments de la ligne de commande, les variables d'environnement, le résultat de certains appels-système (readdir(), readlink(), readdir(), ...), les données provenant de fichiers sont considérés comme a priori suspects, et donc corrompus.

Pour nettoyer une variable, il suffit de la filtrer au travers d'une expression régulière. Bien évidemment, utiliser .* ne sert à rien. Le but de cette opération est de vous forcer à prendre garde aux arguments qui vont sont fournis.

Cependant, ce mode ne protège pas de tout : la corruption des arguments passés sous forme de liste à system() ou exec() n'est pas vérifiée. Vous devez donc être particulièrement méticuleux si un de vos scripts utilise ces fonctions. L'instruction exec "sh", '-c', $arg; est considérée comme sécurisée, que $arg soit corrompu ou pas :(

Il est aussi fortement conseillé d'ajouter un "use strict;" au début de vos programmes. Cela oblige à déclarer vos variables, certains vont trouver ça pénible, mais c'est obligatoire si vous faites du mod-perl.

Ainsi vos scripts CGI en Perl doivent commencer par :

#!/usr/bin/perl -wT
use strict;
use CGI;
ou depuis Perl 5.6.0 par :
#!/usr/bin/perl -T
use warnings;
use strict;
use CGI;

Appel à open()

De nombreux programmeurs ouvre un fichier simplement avec open(FILE,"$filename") || .... Nous avons déjà vu les risques liés à ceci. Pour les réduire, il suffit de préciser le mode d'ouverture :

Evitez donc d'ouvrir vos fichier de manière non précisée.

Avant d'accéder à un fichier, il est conseillé de vérifier aussi que le fichier en question existe bien. Ceci ne protège pas des problèmes de condition de concurrence (race conditions) que nous avons étudiés dans le dernier article, mais permet d'éviter certains pièges comme les commandes avec des arguments.

if ( -e $filename ) { ... }

Depuis Perl 5.6, il existe une syntaxe supplémentaire pour open() : open(FILEHANDLE,MODE,LIST). Avec le mode '<', le fichier est ouvert en lecture ; avec le mode '>' le fichier est tronqué, ou créé au besoin, puis ouvert en écriture. Cela devient intéressant pour les modes où l'on communique avec d'autres processus. Si le mode est '|-' ou '-|', l'argument LIST est interprété comme une commande et se trouve respectivement après ou avant le pipe.

Avant l'apparition de Perl 5.6 et du open() à trois arguments, certaines personnes avaient recours à la commande sysopen().

Échappement des entrées - Filtrage des entrées

Il y a deux méthodes: soit on spécifie les caractères interdits, soit on définit explicitement les caractères autorisés à l'aide d'expressions régulières. Les programmes mis en exemple devraient vous avoir convaincus qu'il est facile d'oublier de filtrer des caractères potentiellement dangereux, c'est pourquoi la seconde méthode est conseillée.

En pratique, voici ce que cela donne : on commence par vérifier que la requête ne comporte que des caractères autorisés. Ensuite, on échappe ceux considérés comme dangereux parmi ces caractères autorisés.

#!/usr/bin/perl -wT

# filtre.pl

#  Les variables $safe et $danger définissent respectivement l'ensemble des
#  caractères sans risque et celui des caractères dangereux.
#  Il suffit d'en ajouter/enlever pour changer le filtre.
#  Seules les $input comportant des caractères inclus dans l'union de 
#  ces ensembles sont valides.

use strict;

my $input = shift;

my $safe = '\w\d';
my $danger = '&;`\'\\|"*?~<>^(){}\$\n\r\[\]';
#Note:
#  Le '/', l'espace et la tabulation ne sont volontairement 
#  dans aucun des deux ensembles


if ($input =~ m/^[$safe$danger]+$/g) {
    $input =~ s/([$danger]+)/\\$1/g;
} else {
    die "Bad input chars in $input\n";
}
print "input = [$input]\n";

Ce script définit deux ensembles de caractères :

Toute requête recelant un caractère n'appartenant à aucun de ces deux ensembles est immédiatement rejetée.

Scripts PHP

Sans vouloir déclencher une polémique, il est à mon sens souvent préférable de choisir d'écrire ses scripts en PHP plutôt qu'en Perl. Plus exactement, en tant qu'administrateur-système, je préfère autoriser mes utilisateurs à utiliser le langage PHP plutôt que le langage Perl. Une personne programmant mal - de façon non-sécurisée - en Perl le fera aussi en PHP. Alors pourquoi préférer le PHP ? Même si on retrouve certains problèmes de programmation en PHP, on peut activer le Safe mode (safe_mode=on) ou bien désactiver certaines fonctions (disable_functions=...). Ce mode empêche d'accéder aux fichiers n'appartenant pas à l'utilisateur, de modifier les variables d'environnement sauf autorisation explicite, d'exécuter des commandes, etc.

Par défaut, la bannière d'Apache nous renseigne sur la présence éventuelle de PHP.

$ telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.localdomain.
Escape character is '^]'.
HEAD / HTTP/1.0
 
HTTP/1.1 200 OK
Date: Tue, 03 Apr 2001 11:22:41 GMT
Server: Apache/1.3.14 (Unix)  (Red-Hat/Linux) mod_ssl/2.7.1
        OpenSSL/0.9.5a PHP/4.0.4pl1 mod_perl/1.24
Connection: close
Content-Type: text/html

Connection closed by foreign host.
Il suffit d'un expose_PHP = Offdans /etc/php.ini pour la masquer :
Server: Apache/1.3.14 (Unix)  (Red-Hat/Linux) mod_ssl/2.7.1 
OpenSSL/0.9.5a mod_perl/1.24 

Le fichier /etc/php.ini (PHP4) ou /etc/httpd/php3.ini recèle de nombreux paramètres permettant de durcir le système. Par exemple, l'option "magic_quotes_gpc" ajoute des quotes sur les arguments reçus par les méthodes GET, POST et via les cookies, cela élimine déjà un bon nombre de problèmes présentés dans nos exemples en Perl.

Conclusion

De tous les articles présentés, celui-ci est certainement le plus facilement compréhensible. Il montre des failles exploitables tous les jours sur le web. Il en existe d'autres, souvent liées à une mauvaise programmation (par exemple, un script qui envoie un mail, prenant en argument le champ From:, offre un sympathique moyen de spam). Les exemples sont (trop) nombreux. A partir du moment où un script se retrouve sur un site web, il y a fort à parier qu'au moins une personne cherchera à l'exploiter de manière malveillante.

Il termine notre série sur le thème de la programmation sécurisée. Nous espérons vous avoir fait découvrir les principales failles de sécurité présentes dans de trop nombreuses applications, et surtout que cela vous incitera à bien prendre en compte le paramètre "sécurité" lors de la conception et la programmation de vos applications. Les problèmes de sécurité sont trop souvent négligés en raison de la portée limitée du développement (usage interne, installation sur un réseau privé, maquette temporaire, etc.). Et pourtant, même un module à diffusion très restreinte peut servir un jour de base à une application plus conséquente, pour laquelle les modifications ultérieures se révéleront largement plus coûteuses.


ISO 8859-1 Caractère
%00 \0 (fin de chaîne)
%0a \n (retour chariot)
%20 espace
%21 !
%22 "
%23 #
%26 & (ampersand)
%2f /
%3b ;
%3c <
%3e >
Tab 1 : Correspondance entre ISO 8859-1 et caractère

Liens


guestbook.cgi bogué

#!/usr/bin/perl -w

# guestbook.cgi

BEGIN { $ENV{PATH} = '/usr/bin:/bin' }
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};   # Make %ENV safer =:-)
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD><TITLE>Buggy Guestbook</TITLE></HEAD>\n";
&ReadParse(\%input);
my $email= $input{email};
my $texte= $input{texte};
$texte =~ s/\n/<BR>/g;
 
print "<BODY><A HREF=\"guestbook.html\">
       GuestBook </A><BR><form action=\"$ENV{'SCRIPT_NAME'}\">\n
      Email: <input type=texte name=email><BR>\n
      Texte:<BR>\n<textarea name=\"texte\" rows=15 cols=70>
      </textarea><BR><input type=submit value=\"Go!\">
      </form>\n";
print "</BODY>\n";
print "</HTML>";
open (FILE,">>guestbook.html") || die ("Cannot write\n");
print FILE "Email: $email<BR>\n";
print FILE "Texte: $texte<BR>\n";
print FILE "<HR>\n";
close(FILE);
exit(0);
 
sub ReadParse {
  my $in =shift;
  my ($i, $key, $val);
  my $in_first;
  my @in_second;
 
  # Read in text
  if ($ENV{'REQUEST_METHOD'} eq "GET") {
    $in_first = $ENV{'QUERY_STRING'};
  } elsif ($ENV{'REQUEST_METHOD'} eq "POST") {
    read(STDIN,$in_first,$ENV{'CONTENT_LENGTH'});
  }else{
    die "ERROR: unknown request method\n";
  }
 
  @in_second = split(/&/,$in_first);
 
  foreach $i (0 .. $#in_second) {
    # Convert plus's to spaces
    $in_second[$i] =~ s/\+/ /g;
 
    # Split into key and value.
    ($key, $val) = split(/=/,$in_second[$i],2); 
 
    # Convert %XX from hex numbers to alphanumeric
    $key =~ s/%(..)/pack("c",hex($1))/ge;
    $val =~ s/%(..)/pack("c",hex($1))/ge;
 
    # Associate key and value
    $$in{$key} .= "\0" if (defined($$in{$key})); 
    $$in{$key} .= $val;
 
  }
 
  return length($#in_second);
}