Sécuriser un script de formulaire e-mail

16/12/03

My defences, become fences, now i'm stumbling, i change my face and if you think i'm fake up wait around till i take off my make-up.
Tricky, Christiansands

Hébergé sur un serveur mutualisé ou simple, les formulaires qui permettent l'envoi d'e-mails peuvent être source de nombreux problèmes de sécurité. Mal conçus, ils peuvent être exploités par des spammers qui l'utilisent alors comme relais pour leurs envois massifs.

S'il n'y a pas grand chose à faire sur le formulaire lui même, le script qui effectue l'envoi par derrière doit faire l'objet de toutes vos attentions. La moindre faille peut être exploitée par des tiers malveillants.

Recours à sendmail

Sendmail est un programme serveur qui gère les transferts d'e-mails. Si son recours est d'une facilité déconcertante, il peut être victime d'une trop grande simplicité d'usage. Son recours se passe par l'ouverture d'un pipe sur le programme lui même puis le passage de paramètres, les headers et le contenu du message, la fermeture du pipe entrainant l'envoi du message en question.

La première mesure à prendre quand vous appellez sendmail est de le faire un mode -t. Passer des paramètres quelconques en ligne de commande à un utilitaire système peut s'avérer désastreux. L'usage d'un handler vers le système est une faille classique. Ainsi, même si vous essayez de sécuriser les paramètres que vous allez passer au système, vous n'êtes jamais garanti contre ceux qui essayeraient d'un profiter. A partir du moment où vous faites un tel appel, un simple surcharge des opérateurs permet d'éxécuter des commandes système en toute liberté.

Ainsi nous recomandons chaudement le recours à une formule du type open(SENDMAIL, "|/path/sendmail -t") à l'exclusion de toute autre. Il est certes tentant de passer l'adresse du destinataire du message en ligne de commande mais c'est un risque inconsidéré. Le mode -t permet de signifier dans les headers du message uniquement le ou les destinataires du message. Le gros avantage de cette syntaxe est qu'aucune variable n'est passée en paramètre et donc, la porte vers le système est complêtement fermée.

Cela ferme certes à l'usage malicieux qui consiste à envoyer un message à une adresse tout en faisant apparaître une autre adresse dans le header To: de votre message, mais cette pratique est de toute façons à proscrire.

D'une manière plus générale, évitez les appels systèmes avec variables utilisateurs, que ce soit pour des pipes, des system() ou des apostrophes inversées. Vous pouvez de devez mettre en oeuvre des stratégies de vérification des données entrées dans le formulaire mais ne vous faites pas trop confiance.

Si vous ne parvenez pas à sécuriser les variables que vous devez passer à sendmail, changez d'orientation et rabatez vous sur le module Net::SMTP si c'est possible et pas trop encombrant. Pour d'autres tâches, vous trouverez sur le CPAN des modules adaptés, avec la garantie de sécurité qu'offre l'open-source, modules qui remplissent un nombre incroyable de fonctions.

Vérification du Referrer.

Si nous avons dit qu'il n'y avait pas grande chose à faire au niveau du formulaire html lui même, ne serait-ce que pour garder tout de même une certaine flexibilité, il importe pourtant de vérifier son identité.

La variable d'environment HTTP_REFERER (notez au passage la faute d'orthographe dans le mot referrer dans les protocoles standard) est censée fournir l'adresse du formulaire qui fait appel au script. Nous disons "censée" car il existe des manoeuvres possibles pour mettre en echec cette variable. En supposant que ces manoeuvres n'ont pas été mises en oeuvre, nous allons voir comment il est possible de procéder à quelques vérifications.

Si l'on estime que les seules pages authorisées à appeller le script en question se trouvent sur notre domaine, on peut commencer par vérifier que le referrer commence par habett.com ou www.habett.com. Ainsi on codera la condition suivante: unless (($referrer =~ /^http:\/\/habett\.org\/.*/) or ($ref =~ /^http:\/\/www\.habett\.org\/.*/)) de manière à s'assurer que l'url de la page qui appelle le script est bien positionnnée sur le serveur en question. Notez bien au passage que les deux possibilités sont prises en compte et qu'un / a été ajouté en fin, en plus du préfixe http:// ce qui offre une palette d'usages suffisament large.

Cependant cette mesure peut être détournée en ayant recours à un formulaire dont l'adresse serait de la forme http://habett.com/@spammer.net/form.html qui utilise la syntaxe réservée aux pages html ayant recours à un nom d'utilisateur. Cette mise en oeuvre est simple et efficace pour tromper une vérification de referrer simple. Pour contrer cet usage, il suffit de tester l'absence de tout @ dans l'url referrer en ajoutant and !($ref =~ /@/)) dans notre test unless.

On peut maintenant estimer que la protection du referrer est efficace, tout en gardant à l'esprit que ce simple test n'est pas suffisant car cette variable d'environment peut être hackée. Si votre script n'est appellé que par un seul formulaire alors, bien évidement, codez en dur dans le script son url.

Dans le même ordre de mesures, vérifiez toujours la présence des champs que vous avez prévu et uniquement de ceux-ci à l'exclusion de tous autres. Cela n'est pas toujours possible mais si c'est efficace.

Contre mesures génériques.

Que faire si vous avez identifié un referrer incorrect ? Pour notre part nous pensons que le plus simple est de renvoyer l'utilisateur d'où il vient c'est à dire de sortir un print "Location: $ref \n\n"; puis de terminer le programme. C'est une méthode simple et efficace. Si l'on veut aller plus loin et prendre des mesures préventives, on peut consigner dans un fichier de logs les mauvais referrers afin de garder une trace des appels non authorisés.

Si l'on constate que cette opération est infructueuse, on peut consigner dans un log tous les referrers afin de les étudier en série et d'en tirer les leçons.

La contre mesure du retour à l'envoyeur peut sembler futile voire inutile, mais elle est simple et efficace dans la plupart des cas. Certains préferreront sans doute terminer simplement le traitement pour que le client indésirable obtienne une erreur http mais faire croire à un pirate qu'il a réussi peut être une bonne psychologie.

Sécuriser les variables.

Quand vous utilisez un formulaire mail, certains éléments du message à envoyer peuvent dépendre de variables envoyées par le formulaire. A chaque fois que cela est possible, codez les éléments en dur dans votre script mais ce n'est pas toujours possible, surtout si l'on veut un script flexible.

Conssidérez un formulaire de contact qui vous envoie un message avec l'identité de l'expéditeur. Vous allez coder en dur le destinataire To:, vous, mais vous allez insérer l'adresse e-mail de l'expéditeur dans le champ From: pour faciliter par la suite vos correspondances. Tout fonctionne à merveille si l'utilisateur entre dans le formulaire en question son adresse mais imaginez un utilisateur malicieux qui y entre une adresse, un caractère de nouvelle ligne, puis une ligne qui commence par CC: ou BCC: suivi d'autres adresses e-mail. En utilisant cette manoeuvre, le message vous est envoyé ainsi qu'à d'autres personnes. En surchargeant de plusieurs lignes, il peut modifier le To: et parvenir à ses fins d'envoyer le message à un grand nombre de destinataires.

Nous espérons vous avoir démontré qu'il est important de sécuriser toutes les variables que vous allez insérer dans les headers du message. Si dans notre cas, cet exemple s'applique au From:, n'oubliez pas non plus qu'une manoeuvre équivalente est possible avec le champ Subject:, To: ou n'importe quel élément des headers. Bref, ne jamais reprendre de variables utilisateur dans les headers sans les avoir inspectées.

Sécurisation d'une adresse

Pour sécuriser une adresse e-mail à présenter dans les headers, que ce soit en From: qu'en To:, il faut donc un traitement préalable. Nous vous proposons le traitement suivant:

  $totreat = lc $totreat;
  $totreat =~ s/(.*)\n.*/$1/;
  $totreat =~ s/,/ /;
  $totreat =~ s/[^a-z0-9_\-\.@]/ /g;
  $totreat =~ s/^ +//;
  $totreat =~ s/ .*//;

Il consiste à passer l'adresse (présupposée) en minuscule, de supprimer tout ce qui suit un éventuel saut de ligne, de remplacer les virgules par des espaces et d'interdire tout caractère autre que ceux présents dans les adresses e-mail à savoir les lettres, les chiffres, les underscore, les tirets, les points et les arobases. On finit par supprimer les espaces au début et tout ce qui suivrait un autre espace dans l'adresse. Ainsi on peut estimer avoir une adresse sécurisée.

Sécuriser des données

Pour les autres données devant apparaître dans les headers, il suffit simplement d'un $totreat =~ s/(.*)\n.*/$1/; pour s'assurer que les données sont bien cantonées à une ligne. On procèdera ainsi par exemple pour le titre d'un message, Subject: qui sera ainsi assuré de prohiber toute surcharge.

Pour ce qui est du corps du message lui même, pas besoin de les sécuriser à priori, même si cela ne peut pas faire de mal.

Résolution des IPs

Dans nos messages, nous aimons bien ajouter une petite résolution de l'IP de l'envoyeur. C'est un peu inutile mais cela permet de savoir à qui on a affaire dans certains cas. Comme vous le savez, la variable d'environment REMOTE_ADDR doit contenir l'adresse IP de la personne qui soumet le formulaire. Il est possible dans un certain nombre de cas d'obtenir l'équivalent textuel de cette adresse numérique composée de quatre nombres sur huit bits sous la forme 192.168.0.1. Par exemple, motre adresse IP lors de la rédaction de cet article peut être résolue en ATuileries-XXXX-X-XX-XX.wXX-XX.abo.wanadoo.fr indiquant ainsi le provider utilisé ainsi que d'autres informations, dans cet exemple géographiques.

En ayant préalablement utilisé un use Socket; et initialisé la variable $ip = $ENV{'REMOTE_ADDR'};, on peut la résoudre en faisant un petit $name = gethostbyaddr(inet_aton($ip), AF_INET); puis @addr = gethostbyname($name); alors on obtient les données voulues dans $addr[0].

Nous aimons bien ainsi finir nos messages par une petite ligne où figure l'adresse IP de la personne qui a soumis le formulaire ainsi que sa résolution. Cela permet une identification mais pas dans tous les cas, bien loin de là. Si en général les adresses IP des personnes utilisant des providers grand public se résolvent bien, celles utilisées par les entreprises et les providers spécialisés se résolvent rarement.

A noter que la résolution peut normalement être aussi obtenue par la variable d'environment REMOTE_HOST.

>

Vous vous demandez peut-être le rapport que cela peut avoir avec la sécurité et bien il est double. Tout d'abord, si le hacker ne mérite pas son nom, son identité apparaîtra en clair. Ensuite si le hacker est un spammer professionnel alors il n'aimera pas que l'on vienne polluer son message avec une petite ligne maison et cela peut être fait avec n'importe quel texte de votre confection, la résolution de l'IP n'étant ici qu'un simple exemple dynamique.

Monitorer les IPs qui accèdent à votre script.

Enfin, si toutes vos précautions ont échoué ou si vous voulez complêter votre sécurité, créez un logs des dernières adresses IP ayant accédé à votre script et vérifiez qu'il n'y a pas trop de redondances. Un pirate qui trouverait une faille dans votre dispositif ne s'y interesserait que si elle est rentable en termes de productivité. Il va donc chercher à utiliser cette faille un grand nombre de fois à la suite. Créez un mini log des IPs qui appellent le script (variable d'environment REMOTE_ADDR) et assurez-vous à chaque éxécution qu'il n'y a pas de redondances douteuses. Certes le mauvais client peut effectuer une rotation d'IPs pour ses appels malicieux mais c'est tout de même une protection utile. Si votre log est suffisament développé, vous pourez peut-être y détecter des motifs suspects.

Autre procédures

Les spammers qui vont essayer d'exploiter les faiblesses de vos scripts cgi ne vont pas le faire à la main mais au moyen de robots ou scripts qu'ils feront tourner eux-même. Il peut ainsi être judicieux de placer une étape supplémentaire de vérification des données entrées. Cela rajoute une étape et donc cela complique la tache du pirate. Si cette confirmation est doublée de l'adjonction puis de la vérification d'un cookie, cela peut peut-être contribuer à éliminer l'utilisation par un robot.

Comme nous l'avons dit, toutes les variables d'environment peuvent être feintes par un script bien écrit, c'est pourquoi il importe toujours de vérifier qu'elles ne sont pas vides et qu'elles contiennent bien des informations senssées. Par exemple, vérifiez que le visiteur utilise bien un véritable browser, ou à défaut un script bien écrit, en regardant le contenu de la variable d'envirnment HTTP_USER_AGENT.

Un hacker trouvera son travail facilité s'il peut savoir comment fonctionne le script sur lequel il travaille. Ainsi les scripts compilés présentent un certain avantage. Si vous préférez les languages interprétés type perl comme nous, alors il faut être sûr qu'en aucune circonstance un visiteur ne pourait accéder au fichier du script en question autrement qu'en mode éxécution. Il est donc trés important d'avoir un politique stricte quand aux droits liés à vos scripts. Une mauvaise configuration du serveur ou un mauvais chmod peut être désastreux.

Conclusion

En termes de sécurité, on ne va jamais assez loin mais mieux vaut prendre toutes les précautions utiles plutôt que d'être une victime passive. En tout état de cause, une fois votre système en place, essayez de le hacker vous même et voyez ce que vous pouvez contrer. En tout point, si vous sentez un maillon faible et que vous êtes victimes de mauvais clients, créez un log de ces éléments et voyez ce que vous pouvez faire. Tout n'est pas possible mais il n'est pas correct vis à vis de votre hébergeur et à fortiori de vous même de ne pas tout essayer.

Nous sommes tous quelque chose de naissance, musicien ou assassin, mais il faut apprendre le maniement de la harpe ou du couteau.
Augustin Vidovic

Depuis que j'ai mis en place ces mesures, j'ai récupéré et épargné plus de 21Mo d'e-mails.