Criando programas em Perl de forma segura

Alceu Junior
Publicado em 22/02/2007

r6 - 22 Feb 2007 - AlceuJunior

Criando programas em Perl de forma segura

Técnicas para criar programas em Perl de forma segura.

Motivações

Este texto tem a intenção de servir com um tutorial de como escrever programas em Perl que eventualmente precisem executar um outro programa, seja para capturar a saída do mesmo ou depender de alguma atividade que ele executa e você não quer reproduzir isso em código Perl por diversos motivos. Eu escrevi este texto por dois motivos basicamente:

1 - Cansei de ver mensagens na lista sobre scripts ruins disponíveis no Matt's script archive (se você está procurando um repositório de scripts prontos então evite o Matt Script Archive, aonde você encontra exemplos realmente bons de como não programar em Perl; uma alternativa muito melhor é o projeto NMS).

2 - Perl é uma linguagem famosa como glue language no entanto não existe nenhum documento em português sobre o como executar outros programas de forma segura (pelo menos até o término deste artigo).

Este texto foi redigido originalmente no Vim, um programa felizmente de uso livre. Nenhum bit foi ferido durante o processo.

Programação segura

Programar de forma segura é, acima de tudo, um hábito. Mesmo que a linguagem de programação ofereça recursos para programar desta forma, geralmente isso é opcional. Na realidade, existem poucos casos em que você talvez não precise se preocupar com isso. Vou citar dois exemplos em que talvez isso funcione:

1 - Você desenvolve em Perl apenas para você (seu egoísta!).

2 - Você trabalha sozinho e é o único ser vivo, racional, que tem acesso ao computador aonde seus programas em Perl residem.

Em outros casos você deveria se preocupar sobre o quanto seu programa é seguro para ser usado. Usuários comentem bobagens às vezes por descuido ou inocência. Outras vezes as cometem por maldade mesmo. Prever o dano que seu programa pode causar se usado de forma errônea é um risco que você sempre deve analisar caso seja responsável pelos programas que escreve.

O básico

Existem dois cuidados básicos para qualquer programa que você for escrever em Perl:

* Sempre verifique a entrada de dados feitas pelo usuário ou por outro programa que execute seu script Perl;

Isso é até considerado muito básico. Você deve sempre testar a entrada de dados do seu usuário. Primeiro porque você pode assim evitar que o programa se comporte de maneira errada e ainda corrigir o usuário. Segundo, se a entrada de seu usuário influir diretamente no comando de sistema que você quer executar você poderá se deparar com coisas assim:

 my $entrada = shift;
 system("cat /etc/passwd | grep $entrada");

Esse é um exemplo ingênuo, mas funcional. Isso mostraria uma ou mais linhas de respostas baseadas no critério de pesquisa do comando grep. Agora imagine que a entrada do usuário seria essa:

 # programa.pl > /etc/shadow'

Assim que o Perl substitui-se o valor da variável $entrada, você teria o conteúdo do arquivo de senhas substituído pelo conteúdo de /etc/passwd. Isso provavelmente seria algo prejudicial para seu programa e para o sistema em si.

* Sempre procure encontrar módulos em Perl no CPAN que possam estar executando a tarefa que você quer realizar. Isso evita chamadas ao sistema, o que é mais seguro e muitas vezes mais rápido. Supondo que você seja preguiçoso (e isso é uma virtude para programadores, segundo Larry Wall) você não quer reinventar a roda: quer apenas resolver seu problema. Isso é louvável. Mas procure sempre encontrar um substituto escrito em Perl para o programa que você queira rodar. Para tarefas simples e corriqueiras, como executar um 'tail' no shell, existe muitos módulos em Perl que já lhe oferecem isso, e estão disponíveis no CPAN. Tudo que você tem que fazer é baixar e instalar esses módulos (vide perldoc CPAN para maiores informações de como fazer isso). Tendo essas tarefas sendo executadas pelo próprio interpretador do Perl, você não precisa fazer chamadas de sistemas, o que é mais rápido (na maioria das vezes), seguro e limpo.

Revendo as técnicas básicas

Existem diversas formas que se pode utilizar em Perl para executar programas de sistema:

* a crase;
* system;
* open;
* exec;
* IPC::Open2;
* IPC::Open3

Alguns desses comandos você usará apenas para executar um programa. Outros, para capturar a saída de um programa ou ainda fornecer dados para esse programa através da entrada padrão dele (STDIN). Outros ainda permitem executar o comando e capturar a saída comum e de erro do mesmo.

Com certeza devem existir outras formas de fazer isso. Mas estas são as clássicas, se você resolver utilizar outra tente abstrair o que leu aqui para essa outra forma.

Não usarás!

Algum comandos (ou uma de suas formas de utilização) vão chamar o shell para interpretar e executar o programa. Isso inclui a interpretação de coisas como "*", "?", "<", ">", etc... Você deve se lembrar o quanto esses metacaracteres podem ser perigosos.

Existem algumas coisas que você simplesmente não poderá usar quando quiser escrever um programa seguro:

* crase

Tal como o shellscript, Perl permite que você capture a saída de execução de um programa para uma variável utilizando a seguinte forma:

 $var = `who`

Mas usar a crase significa que a Perl sempre vai chamar o shell para interpretar o comando e devolver a resposta.

* system com apenas um argumento

É uma má idéia usar system dessa forma:

 system("algum comando para executar");

Quando chamado dessa forma, a Perl passa novamente seu comando para o shell interpretar, incluindo os parâmetros.

* open

Usar open para servir de entrada padrão para um programa ou ler a saída dele. Isso também é executado no shell, mas existe uma outra forma (que apresentarei logo a seguir) para usar o open sem esses problemas.

Usarás (e com cuidado)!

* Os comandos listados abaixo não usam a shell, então você deve considerar usá-los sempre que lhe for possível.

Novamente o system. Quando utilizado com a sintaxe seguinte:

 system("comando","argumento1","argumento2");

o system não chama mais o shell para interpretar o comando, sendo executado diretamente pela Perl.

* A dobradinha open e exec

Quando se junta os comandos open e exec para capturar a saída/enviar entrada de/para um programa, a shell não é mais executada. Isso acontece quando se usa a sintaxe do open para fazer um fork e criar um processo filho. O exec (o processo filho) então executa o comando e seus argumentos sem chamar a shell para interpretá-los. A sintaxe é a seguinte:

 open(FOO, '|-') || exec 'tr', '[a-z]', '[A-Z]';
 open(FOO, '-|') || exec 'cat', '-n', $file;

Respectivamente, direcionando a saída do programa Perl para o programa tr e redirecionando a saída do cat para seu programa em Perl.

* usando o exec

O exec nunca chama o shell para executar um comando. Mas também não retorna nada de volta, o que é meio triste por sinal. Mas pelo menos funciona.

* IPC::Open2

O open2 faz parte do módulo IPC, presente por padrão em qualquer distribuição Perl que não tenha sido compilada na era Paleozóica. Imagine ele como o open com esteróides. Ele pode executar um programa sem usar o interpretador de comandos (desde que você passe os parâmetros em forma de lista), e ainda vai lhe fornecer acesso controlado ao STDIN e STDOUT do programa executado. Isso significa que você pode empurrar coisas a vontade para o programa tentar digerir (em STDIN) e ler/processar/omitir a saída de STDOUT.

Mas nem tudo é belo: seu programa pode travar se tentar ler a entrada/saída do programa sendo executado. Isso pode ocorrer caso o script tente ler uma entrada ou uma saída que nunca é fornecida ou é utilizada em excesso. Você pode evitar esse travamento utilizando a função select ou o módulo IO::Select, que oferece acesso as operações da função select mas através de uma interface orientada a objetos. Segue um exemplo (extraído a documentação do módulo):

 use IPC::Open2;

 # Exemplo 1
 # essa é a forma incorreta de se evitar o shell!
 $pid = open2(\*RDRFH, \*WTRFH, 'some cmd and args');

 # Exemplo 2
 # esta é a forma correta
 $pid = open2(\*RDRFH, \*WTRFH, 'some', 'cmd', 'and', 'args');

* IPC::Open3

O open3 faz as mesmas coisa que o open2, com adição de poder trabalhar também com STDERR do programa sendo executado. Infelizmente, o open3 também está sujeito aos mesmos problemas do open2. Um exemplo (também extraído da documentação do módulo):

 use IPC::Open3;
 use strict;
 use warnings;

 my($wtr, $rdr, $err);
 $pid = open3($wtr, $rdr, $err,'some cmd and args', 'optarg', ...);

Onde $wtr, $rdr e $err são respectivamente STDIN, STDOUT e STDERR do programa sendo executado. Particularmente eu gosto do open3 para ter maior controle sobre a aplicação sendo executada: normalmente o programa pode usar STDOUT e STDERR para emitir mensagens e elas podem ser necessárias para controle do script ou ainda precisam ser omitidas. Ao invés de trabalhar com STDIN, STDOUT e STDERR de forma separada, você pode simplesmente fazer assim:

 use IPC::Open3;
 use strict;
 use warnings;

 my $pid = open3(\*WTRFH,\*FROMCHILD,\*FROMCHILD,"ps","ax");

 while (  ) {print;}

 close(FROMCHILD);
 close(WTRFH);

Entendeu o truque? Estamos usando STDOUT e STDERR com a mesma referência, o que nos permite ler os dois handles de uma vez só.

Outros cuidados

Existem muitos cuidados que você ter quando quiser escrever um programa Perl de modo que ele funcione com uma relativa segurança. Algumas coisas são relativas a cuidados de arquitetura, como race conditions. Outros são cuidados mínimos, como usar a opção "Taint" do Perl. A opção Taint funciona como uma pragma do Perl, assim como warnings e strict. Você pode invocar o modo Taint dessa forma:

 #!/usr/bin/perl -T

ou ainda assim:

 # perl -T script_perl

Quando o modo Taint for ativado, o Perl vai assumir que quaisquer outros dados vindos de fora do programa, como variáveis definidas pela entrada do usuário em tempo de execução, são marcadas como corrompidas e o programa é interrompido no ato quando é tentado usar essa variável para coisas perigosas, como definir um nome de arquivo a ser criado. Para resolver isso, você deve "lavar" os dados para que a Perl passe a aceitá-lo:

 use strict;
 use warnings;

 my $arquivo = shift;

 unless ($arquivo =~ /(^[\w\.\-\_]+$)/) {

   die "Nome de arquivo inválido\n";

 } else {

   $arquivo = $1;

 }

Essas linhas somente permitirão que um nome de arquivo seja composto apenas de letras, números, "-", "_" e pontos ("."). Qualquer outra coisa é tida como inaceitável (como um ">") e o programa é abortado.

Você sempre tem que "lavar" os dados dessa maneira e dessa forma o modo Taint vai aceitar a variável caso ela não cai na ocorrência de possuir um caracter indesejado. É importante cuidar para que a expressão regular realmente elimine qualquer outra coisa que não o aceitável. Se você tentar enganar o modo Taint, vai apenas enganar a si mesmo.

O modo Taint também o ajuda a evitar uma porção de outras coisas também. Procure ler sua documentação perldoc perlsec para maiores detalhes de como essa pragma pode lhe ajudar.

Usar o modo Taint às vezes pode ser algo chato de se fazer. Afinal, você pode ter que começar a lavar uma porção de variáveis que antes não estavam causando nenhum problema em seu script. Mas fazer algo bem feito exige esforço, então tente não ser preguiçoso nesse sentido.

Bibligrafia

* perldoc -f open
* perldoc IPC::Open2
* perldoc IPC::Open3
* perldoc -f select
* perldoc IO::Select
* perldoc perlsec

AUTHOR

Alceu Junior

blog comments powered by Disqus