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
, 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=20En fait, ceci est beaucoup plus simple qu'il n'y paraît. Décomposons cette URL :
www.linuxdoc.org
;
/cgi-bin/ldpsrch.cgi
;
?
signale le début d'une longue liste d'arguments :
srv=http%3A%2F%2Fwww.linuxdoc.org
est le serveur d'où est
émise la requête ;
srch=ssh
contient la requête à proprement parler ;
db=1
signifie que la requête ne porte que sur les
HOWTOs ;
scope=0
stipule que la recherche concerne également le
contenu du document, et pas seulement son titre ;
rpt=20
limite à 20 le nombre de résultats affichés.
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 Unicode. 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 tel caractères.
SSI Server Side Include
"Server Side Include
est une fonction intégrée aux
serveurs web. Il 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 :
Options Includes
active tous les SSI ;
OptionsIncludesNoExec
interdit le exec cmd
et le
exec cgi
. 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
< et >. Il suffit alors à une personne curieuse de soumettre une
des instructions suivantes :
<!--#printenv -->
(attention à l'espace après
printenv
)
<!--#exec cmd="cat /etc/passwd"-->
guestbook.cgi?email=pappy&texte=<%21--%23printenv%20-->
,
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=pappy&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 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><TITLE>Remote Command</TITLE></HEAD>\n"; &ReadParse(*input); # #################################### # # Début du code descriptif du problème # # #################################### # # ################################## # # Fin du code descriptif du problème # # ################################## # form: print "<form action=\"$ENV{'SCRIPT_NAME'}\">\n<input type=texte name=filename>\n </form>\n"; print "</BODY>\n"; print "</HTML>"; exit(0); sub ReadParse { local (*in) = @_ if @_; my ($i, $key, $val); my $in_first; my @in_second; # Read in text if (&MethGet) { $in_first = $ENV{'QUERY_STRING'}; } elsif ($ENV{'REQUEST_METHOD'} eq "POST") { read(STDIN,$in_first,$ENV{'CONTENT_LENGTH'}); } @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); # splits on the first =. # 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})); # \0 is the multiple separator $in{$key} .= $val; } return length($#in_second); } sub MethGet { return ($ENV{'REQUEST_METHOD'} eq "GET"); }
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.
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 contemple 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
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=../../../etc/passwd
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 la
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éussi car il ne portera que sur "../../../bin/ls
" mais
le open()
verra bien le pipe et exécutera la commande. Ainsi, l'URL
fournit le contenu du répertoire courant :
pipe2.cgi?filename=../../../bin/ls%00|
Le script finger.cgi
(source)
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 :
&&
: si la première instruction réussi
(i.e. renvoie 0 en shell), alors la suivante est exécutée ;
||
: si la première instruction échoue (i.e.
renvoie une valeur non nulle en shell), alors la suivante est exécutée.
Le script finger.cgi
précédent prend des mesures pour éviter de
se voir passer des caractères étranges. Ainsi, l'URL 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'URL 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/passwdLes erreur 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
';
' : finger.cgi?login=kmaster\;cat%20/etc/passwd
.
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/passwdLe shell décompose ceci en deux instructions :
/usr/bin/finger kmaster\
qui a de forte chance d'échouer ...
mais ça n'est pas notre problème ;-)
cat /etc/passwd
qui affiche le fichier de mots de passe.
\
' doit également être échappé.
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
(source)
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"; $CMD= "/usr/bin/finger \"$login\"|"; #Nouvelle super protection (in)efficace open(FILE,"$CMD") || goto form; while(<FILE>) { print; }
L'URL qui permet alors d'exécuter notre commande devient :
finger2.cgi?login=kmaster%22%3Bcat%20%2Fetc%2Fpasswd%3B%22
/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.
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 quoique ce soit en dehors du programme.
En mode corruption, les arguments de la ligne de commande, les variables
d'environnement, le résultats 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é, que $arg
soit corrompue 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;
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 :
open(FILE,"<$filename") || ...
pour la lecture
seule ;
open(FILE,">$filename") || ...
pour l'écriture
seule ; 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é 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'agument 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()
.
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 :
$safe
contient ceux considérés comme sans risque (ici,
uniquement des chiffres et des lettres) ;
$danger
contient les caractères à échapper car
potentiellement dangereux. 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é - 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 = Off
dans /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.
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 est 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.
Unicode | Caractère |
%00 | \0 (fin de chaîne) |
%0a | \n (retour chariot) |
%20 | espace |
%21 | ! |
%22 | " |
%23 | # |
%26 | & (ampersand) |
%2f | / |
%3b | ; |
%3c | < |
%3e | > |
man perlsec
: la page man de Perl sur la sécurité ;
#!/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'}\">\nEmail: <input type=texte name=email><BR>\nTexte:<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 { local (*in) = @_ if @_; my ($i, $key, $val); my $in_first; my @in_second; # Read in text if (&MethGet) { $in_first = $ENV{'QUERY_STRING'}; } elsif ($ENV{'REQUEST_METHOD'} eq "POST") { read(STDIN,$in_first,$ENV{'CONTENT_LENGTH'}); } @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); # splits on the first =. # 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})); # \0 is the multiple separator $in{$key} .= $val; } return length($#in_second); } sub MethGet { return ($ENV{'REQUEST_METHOD'} eq "GET"); }
Christophe BLAESS - ccb@club-internet.fr Christophe GRENIER - grenier@nef.esiea.fr Frédéreric RAYNAL - pappy@users.sourceforge.net