NOM
perlsec - Sécurité de Perl
DESCRIPTION
Perl est conçu pour faciliter une programmation sûre, même lorsqu'il tourne avec des privilèges spéciaux, comme pour les programmes setuid ou setgid. Contrairement à la plupart des shells de ligne de commande, qui sont basés sur de multiples passes de substitution pour chaque ligne du script, Perl utilise un procédé d'évaluation plus conventionnel contenant moins de pièges cachés. De plus, comme le langage a plus de fonctionnalités intégrées, il doit moins se reposer sur des programmes externes (et potentiellement peu sûrs) pour accomplir ses tâches.
Perl met en oeuvre automatiquement un ensemble de vérifications spécifiques à la sécurité, appelé taint mode (mode souillé, NDT), lorsqu'il détecte que son programme tourne avec des identifiants de groupe ou d'utilisateurs réel et effectif différents. Le bit setuid dans les permissions d'Unix est le mode 04000, le bit setgid est le mode 02000 ; ils peuvent être placés l'un ou l'autre, ou les deux à la fois. Vous pouvez aussi valider le taint mode explicitement en utilisant l'option de ligne de commande -T. Cette option est fortement conseillée pour les programmes serveurs et pour tout programme exécuté au nom de quelqu'un d'autre, comme un script CGI. Une fois que le taint mode est activé, il l'est pour tout le reste de votre script.
Lorsqu'il est dans ce mode, Perl prend des précautions spéciales appelées taint checks (vérification de pollution, NDT) pour éviter aussi bien les pièges évidents que les pièges subtils. Certaines de ces vérifications sont raisonnablement simples, comme vérifier que personne ne peut écrire dans les répertoires du path ; les programmeurs précautionneux ont toujours utilisé de telles méthodes. D'autres vérifications, toutefois, sont mieux supportées par le langage lui-même, et ce sont ces vérifications en particulier qui contribuent à rendre un programme Perl set-id plus sûr qu'un programme équivalent en C.
Vous ne pouvez pas utiliser des données provenant de l'extérieur de votre programme pour modifier quelque chose d'autre à l'extérieur -- au moins pas par accident. Tous les arguments de ligne de commande, toutes les variables d'environnement, toutes les informations locales (voir perllocale), résultent de certains appels au système (readdir(), readlink(), la variable de shmread(), les messages renvoyés par msgrcv(), le mot de passe, les champs gecos et shell des appels getpwxxx()), et toutes les entrées par fichier sont marquées comme "souillées". Les données souillées ne peuvent pas être utilisées directement ou indirectement dans une commande qui invoque un sous-shell, ni dans toute commande qui modifie des fichiers, des répertoires ou des processus (Exception importante: si vous passez une liste d'arguments soit à system
soit à exec
, la sûreté des éléments de cette liste n'est PAS vérifiée). Toute variable fixée à une valeur dérivant de données souillées sera elle-même souillée, même s'il est logiquement impossible que les données souillées modifient la variable. Puisque la sûreté est associée à chaque valeur scalaire, certains éléments d'un tableau peuvent être souillés et d'autres pas.
Par exemple :
$arg = shift; # $arg est souillée
$hid = $arg, 'bar'; # $hid est aussi souillée
$line = <>; # Souillée
$line = <STDIN>; # Souillée aussi
open FOO, "/home/me/bar" or die $!;
$line = <FOO>; # Encore souillée
$path = $ENV{'PATH'}; # Souillée, mais voir plus bas
$data = 'abc'; # Non souillée
system "echo $arg"; # Non sûr
system "/bin/echo", $arg; # Sûr (n'utilise pas sh)
system "echo $hid"; # Non sûr
system "echo $data"; # no sûr jusqu'à ce que PATH soit fixé
$path = $ENV{'PATH'}; # $path est désormais souillée
$ENV{'PATH'} = '/bin:/usr/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
$path = $ENV{'PATH'}; # $path N'est maintenant PLUS souillée
system "echo $data"; # Est désormais sûr !
open(FOO, "< $arg"); # Ok - fichier en lecture seule
open(FOO, "> $arg"); # Pas Ok - tentative d'écriture
open(FOO,"echo $arg|"); # Pas Ok, mais...
open(FOO,"-|")
or exec 'echo', $arg; # Ok
$shout = `echo $arg`; # Non sûr, $shout est maintenant souillée
unlink $data, $arg; # Non sûr
umask $arg; # Non sûr
exec "echo $arg"; # Non sûr
exec "echo", $arg; # Sûr (n'utilise pas le shell)
exec "sh", '-c', $arg; # Considéré comme sûr, hélas !
@files = <*.c>; # Douteux (utilise readdir() ou équivalent)
@files = glob('*.c'); # Douteux (utilise readdir() ou équivalent)
Si vous essayez de faire quelque chose qui n'est pas sûr, vous obtiendrez une erreur fatale disant quelque chose comme "Insecure dependency" ou "Insecure $ENV{PATH}". Notez que vous pouvez toujours écrire un system ou un exec non sûr, mais seulement en faisant quelque chose comme le "considéré comme sûr" ci-dessus.
Blanchissage et Détection des Données Souillées
Pour tester si une variable contient des données souillées, et quels usages provoqueraient ainsi un message d'"Insecure dependency", allez voir votre plus proche miroir CPAN pour y chercher le module Taint.pm, qui devrait être disponible à partir de novembre 1997. Ou vous pourriez utiliser la fonction is_tainted() suivante.
sub is_tainted {
return ! eval {
join('',@_), kill 0;
1;
};
}
Cette fonction utilise le fait que la présence de données souillées n'importe où dans une expression rend toute l'expression souillée. Il serait inefficace de tester la sûreté de tous les arguments pour tous les opérateurs. Au lieu de cela, l'approche légèrement plus efficace et conservatrice qui est utilisée est que si une valeur souillée a été accédée à l'intérieur d'une expression, alors la totalité de l'expression est considérée comme souillée.
Mais le test de la pureté ne vous fourni rien d'autre. Parfois, vous devez juste rendre vos données propre. La seule façon d'outrepasser le mécanisme de pollution est de référencer des sous-motifs depuis une expression régulière. Perl présume que si vous référencez une sous-chaîne en utilisant $1, $2, etc., c'est que vous saviez ce que vous étiez en train de faire lorsque vous rédigiez le motif. Cela implique d'utiliser un peu de réflexion -- ne pas simplement tout blanchir aveuglément, ou vous détruisez la totalité du mécanisme. Il est meilleur de vérifier que la variable ne contient que des bons caractères (pour certaines valeurs de "bon") plutôt que de vérifier qu'elle contient un quelconque mauvais caractère. C'est parce qu'il est beaucoup trop facile de manquer un mauvais caractère auquel vous n'avez jamais pensé.
Voici un test pour s'assurer que les données ne contiennent rien d'autre que des caractères de "mots" (alphabétiques, numériques et souligné), un tiret, une arobase, ou un point.
if ($data =~ /^([-\@\w.]+)$/) {
$data = $1; # $data est maintenant propre
} else {
die "Bad data in $data"; # loguer cela quelque part
}
Ceci est assez sûr car /\w+/
ne correspond normalement pas aux métacaractères du shell, et les points, tirets ou arobases ne veulent rien dire de spécial pour le shell. L'usage de /.+/
n'aurait pas été sûr en théorie parce qu'il laisse tout passer, mais Perl ne vérifie pas cela. La leçon est que lorsque vous blanchissez, vous devez être excessivement précautionneux avec vos motifs. Le blanchissage des données à l'aide d'expressions régulières est le seul mécanisme pour nettoyer les données polluées, à moins que vous n'utilisiez la stratégie détaillée ci-dessous pour forker un fils ayant des privilèges plus faibles.
L'exemple ne nettoie pas $data si use locale
est en cours d'utilisation, car les caractères auxquels correspond \w
sont déterminés par la localisation. Perl considère que les définitions locales ne sont pas sûres car elles contiennent des données extérieures au programme. Si vous écrivez un programme conscient de la localisation, et voulez blanchir des données avec une expression régulière contenant \w
, mettez no locale
avant l'expression dans le même bloc. Voir "SÉCURITÉ" in perllocale pour plus de précisions et des exemples.
Options Sur la Ligne "#!"
Quand vous rendez un script exécutable, de façon à pouvoir l'utiliser comme une commande, le système passera des options à perl à partir de la ligne #! du script. Perl vérifie que toutes les options de ligne de commande données à un script setuid (ou setgid) correspondent effectivement à celles placées sur la ligne #!. Certains Unix et environnements cousins imposent une limite d'une seule option sur la ligne #!, vous aurez donc peut-être besoin d'utiliser quelque chose comme -wU
à la place de -w -U
sous ces systèmes (ce problème ne devrait se poser qu'avec les Unix et les environnement proches qui supportent #! et les scripts setuid ou setgid).
Nettoyer Votre Path
Pour les messages "Insecure $ENV{PATH}
", vous avez besoin de fixer $ENV{'PATH'}
à une valeur connue, et chaque répertoire dans le path ne doit pas être modifiable par d'autres que son propriétaire et son groupe. Vous pourriez être surpris d'obtenir ce message même si le chemin vers votre exécutable est complètement explicité. Ceci n'est pas généré parce que vous n'avez pas fourni un chemin complet au programme ; au lieu de cela, c'est parce que vous n'avez jamais défini votre variable d'environnement PATH, ou vous ne l'avez pas défini comme quelque chose de sûr. Puisque Perl ne peut pas garantir que l'exécutable en question n'est pas lui-même sur le point de se retourner pour exécuter un autre programme qui dépend de votre PATH, il s'assure que vous ayez défini le PATH.
Le PATH n'est pas la seule variable d'environnement qui peut poser des problèmes. Puisque certains shells peuvent utiliser les variables IFS, CDPATH, ENV, et BASH_ENV, Perl vérifie que celles-ci sont soit vides, soit propres, lorsqu'il démarre des sous- processus. Vous pourriez désirer ajouter quelque chose comme ceci à vos scripts setid et blanchisseurs.
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; # Rend %ENV plus sûr
Il est aussi possible de s'attirer des ennuis avec d'autres opérations qui ne se soucient pas si elles utilisent des valeurs souillées. Faites un usage judicieux des tests de fichiers quand vous avez affaire à un nom de fichier fournis par l'utilisateur. Lorsque c'est possible, réalisez les ouvertures et compagnie après avoir correctement abandonné les privilèges d'utilisateur (ou de groupe !) particuliers. Perl ne vous empêche pas d'ouvrir en lecture des noms de fichier souillés, alors faites attention à ce que vous imprimez. Le mécanisme de pollution est destiné à prévenir les erreurs stupides, pas à supprimer le besoin de réflexion.
Perl n'appelle pas le shell pour étendre les métacaractères (wildcards, NDT) quand vous passez des listes de paramètres explicites à system et exec au lieu de chaînes pouvant contenir des métacaractères. Malheureusement, les fonctions open, glob, et backtick (substitution de commande avec "`", NDT) ne fournissent pas de telles conventions d'appel alternatives, alors d'autres subterfuges sont requis.
Perl fournit une façon raisonnablement sûre d'ouvrir un fichier ou un tube depuis un programme setuid ou setgid : créez juste un processus fils ayant des privilèges réduits et qui fera le sale boulot pour vous. Tout d'abord, créez un fils en utilisant la syntaxe spéciale d'open qui connecte le père et le fils par un tube. Puis le fils redéfinit son ensemble d'ID et tous les autres attributs dépendant du processus, comme les variables d'environnement, les umasks, les répertoires courants, pour retourner à des valeurs originelles ou connues comme sûres. Puis le processus fils, qui n'a plus la moindre permission spéciale, réalise le open ou d'autres appels système. Finalement, le fils passe les données auxquelles il parvient à accéder à son père. Puisque le fichier ou le tube ont été ouverts par le fils alors qu'il tournait avec des privilèges inférieurs à celui du père, il ne peut pas être trompé et faire quelque chose qu'il ne devrait pas.
Voici une façon de faire des substitutions de commande de façon raisonnablement sûre. Remarquez comment l'exec n'est pas appelé avec une chaîne que le shell pourrait interpréter. C'est de loin la meilleure manière d'appeler quelque chose qui pourrait être sujet à des séquences d'échappements du shell : n'appelez tout simplement jamais le shell.
use English;
die "Can't fork: $!" unless defined $pid = open(KID, "-|");
if ($pid) { # parent
while (<KID>) {
# faire quelque chose
}
close KID;
} else {
my @temp = ($EUID, $EGID);
$EUID = $UID;
$EGID = $GID; # initgroups() est aussi appelé !
# S'assurer que les privilèges sont réellement partis
($EUID, $EGID) = @temp;
die "Can't drop privileges"
unless $UID == $EUID && $GID eq $EGID;
$ENV{PATH} = "/bin:/usr/bin";
exec 'myprog', 'arg1', 'arg2'
or die "can't exec myprog: $!";
}
Une stratégie similaire marcherait pour l'expansion des métacaractères via glob
, bien que vous pouvez utiliser readdir
à la place.
La vérification de la sûreté est surtout utile lorsque, même si vous vous faites confiance de ne pas avoir écrit un programme ouvrant toutes les portes, vous ne faites pas nécessairement confiance à ceux qui finiront par l'utiliser et pourraient essayer de le tromper pour qu'il fasse de vilaines choses. C'est le genre de vérification de sécurité qui est utile pour les programmes set-id et les programmes qui sont lancés au nom de quelqu'un d'autre, comme les scripts CGI.
C'est très différent, toutefois, du cas où l'on ne fait même pas confiance à l'auteur du code de ne pas essayer de faire quelque chose de diabolique. Ceci est le type de confiance dont on a besoin quand quelqu'un vous tend un programme que vous n'avez jamais vu auparavant et vous dit : "Voilà, exécute ceci". Pour ce genre de sécurité, jetez un oeil au module Safe, inclu en standard dans la distribution de Perl. Ce module permet au programmeur de mettre en place des compartiments spéciaux dans lesquels toutes les opérations liées au système sont détournées et où l'accès à l'espace de noms est contrôlé avec soin.
Failles de Sécurité
Au-delà des problèmes évidents qui découlent du fait de donner des privilèges spéciaux à des systèmes aussi flexibles que les scripts, sous de nombreuses versions d'Unix, les scripts set-id ne sont pas sûrs dès le départ de façon inhérente. Le problème est une race condition dans le noyau. Entre le moment où le noyau ouvre le fichier pour voir quel interpréteur il doit exécuter et celui où l'interpréteur (maintenant set-id) se retourne et réouvre le fichier pour l'interpréter, le fichier en question peut avoir changé, en particulier si vous avez des liens symboliques dans votre système.
Heureusement, cette "caractéristique" du noyau peut parfois être invalidée. Malheureusement, il y a deux façons de l'invalider. Le système peut simplement déclarer hors-la-loi les scripts ayant un bit set-id mis, ce qui n'aide pas vraiment. Alternativement, il peut simplement ignorer les bits set-id pour les scripts. Si ce dernier cas est vrai, Perl peut émuler le mécanisme setuid et setgid lorsqu'il remarque les bits setuid/gid bits, par ailleurs inutiles, sur des scripts Perl. Il le fait via un exécutable spécial appelé suidperl qui est invoqué automatiquement pour vous si besoin est.
Toutefois, si la caractéristique du noyau pour les scripts set-id n'est pas invalidée, Perl se plaindra bruyamment que votre script set-id n'est pas sûr. Vous devrez soit invalider la caractéristique du noyau pour les scripts set-id, soit mettre un wrapper C autour du script. Un wrapper C est juste un programme compilé qui ne fait rien à part appeler votre programme Perl. Les programmes compilés ne sont pas sujets au bug du noyau qui tourmente les scripts set-id. Voici un wrapper simple, écrit en C :
#define REAL_PATH "/path/to/script"
main(ac, av)
char **av;
{
execv(REAL_PATH, av);
}
Compilez ce wrapper en un binaire exécutable, puis rendez-it setuid ou setgid à la place de votre script.
Voir le programme wrapsuid dans le répertoire eg de votre distribution de Perl pour une façon pratique de faire ceci automatiquement pour tous vos programmes Perl setuid. Il déplace les scripts setuid dans des fichiers ayant le même nom précédé d'un point, puis compile un wrapper comme celui ci-dessus pour chacun d'entre eux.
Ces dernières années, les vendeurs ont commencé à fournir des systèmes libérés de ce bug de sécurité inhérent. Sur de tels systèmes, lorsque le noyau passe le nom du script set-id à ouvrir à l'interpréteur, plutôt que d'utiliser un nom et un chemin sujets à l'ingérence, il passe /dev/fd/3. C'est un fichier spécial déjà ouvert sur le script, de sorte qu'il ne peut plus y avoir de race condition que des scripts malins pourraient exploiter. Sur ces systèmes, Perl devrait être compilé avec l'option -DSETUID_SCRIPTS_ARE_SECURE_NOW
. Le programme Configure qui construit Perl essaye de trouver cela tout seul, vous ne devriez donc jamais avoir à spécifier cela vous-même. La plupart des versions modernes de SysVr4 et BSD 4.4 utilisent cette approche pour éviter la race condition du noyau.
Avant la version 5.003 de Perl, un bug dans le code de suidperl pouvait introduire une faille de sécurité dans les systèmes compilés en conformité stricte avec la norme POSIX.
Protection de Vos Programmes
Il existe de nombreuses façons de cacher le source de vos programmes Perl, avec des niveaux variables de "sécurité".
Tout d'abord, toutefois, vous ne pouvez pas retirer la permission en lecture, car le code source doit être lisible de façon à être compilé et interprété (cela ne veut toutefois pas dire que le source d'un script CGI est lisible par n'importe qui sur le web). Vous devez donc laisser les permissions au niveau socialement amical de 0755. Ceci laisse voir votre source uniquement aux gens de votre système local.
Certaines personnes prennent par erreur ceci pour un problème de sécurité. Si votre programme fait des choses qui ne sont pas sûres, et s'appuie sur le fait que les gens ne savent pas comment exploiter ces failles, il n'est pas sûr. Il est souvent possible pour quelqu'un de déterminer les failles de sécurité et de les exploiter sans voir le source. La sécurité par l'obscurité, expression désignant le fait de cacher vos bugs au lieu de les corriger, est vraiment une faible sécurité.
Vous pouvez essayer d'utiliser le chiffrement via des filtres de sources (Filter::* sur le CPAN). Mais les craqueurs pourraient être capable de le déchiffrer. Vous pouvez essayer d'utiliser le compilateur de byte code et l'interpréteur décrit ci-dessous, mais les craqueurs pourraient être capables de le décompiler. Vous pouvez essayer d'utiliser le compilateur de code natif décrit ci-dessous, mais les craqueurs pourraient être capable de le désassembler. Ces solutions posent des degrés variés de difficulté aux gens voulant obtenir votre code, mais aucune ne peut définitivement le dissimuler (ceci est vrai pour tous les langages, pas uniquement Perl).
Si vous êtes inquiet que des gens profitent de votre code, alors le point crucial est que rien à part une licence restrictive ne vous donnera de sécurité légale. Licenciez votre logiciel et pimentez-le de phrases menaçantes comme "Ceci est un logiciel propriétaire non publié de la société XYZ. L'accès qui vous y est donné ne vous donne pas la permission de l'utiliser bla bla bla". Vous devriez voir un avocat pour être sur que votre vocabulaire tiendra au tribunal.
VOIR AUSSI
perlrun pour sa description du nettoyage des variables d'environnement.
TRADUCTION
Version
Cette traduction française correspond à la version anglaise distribuée avec perl 5.6.0. Pour en savoir plus concernant ces traductions, consultez http://perl.enstimac.fr/.
Traducteur
Roland Trique <roland.trique@free.fr>
Relecture
Régis Julié <regis.julie@cetelem.fr>