quinta-feira, 5 de março de 2009

Filtragem de caracteres indesejáveis

Hoje o Juliano entrou em contato, pedindo uma ajuda para filtrar caracteres indesejáveis que vinham nos arquivos de dados...

Depois de muito quebrar a cabeça, conseguimos o seguinte:


1. Mudar o locale para português-Brasil

setlocale(LC_ALL,"pt_BR.ISO8859-1");
ou
setlocale(LC_ALL,"pt_BR.UTF-8");

2. Filtrar

$v = preg_replace('/[^[:blank:]|[:space:]|[:alnum:]|[:punct:]]/', ' ', $v);


segunda-feira, 2 de março de 2009

#1 - Validação de Entrada de dados (primeira parte)

Origem dos dados

Originalmente, os programadores PHP acessavam dados enviados pelos usuários através dos mecanismos de "register globals".

Usando "register globals", qualquer parâmetro passado para o script é disponibilizado como uma variável com o mesmo nome do parâmetro.

Embora o mecanismo de "register globals" seja uma abordagem para capturar parâmetros pelos scripts, é uma vulnerabilidade e possível fonte de exploração por parte de hackers.

Um dos problemas é o conflito entre os parâmetros de entrada: Dados fornecidos ao script podem vir de várias fontes, incluindo requisições GET, POST, cookies, variáveis de ambiente do servidor e variáveis de ambiente do sistema - nenhuma delas é exclusiva.

Então, se as fontes são muitas, o PHP é forçado a mesclá-las, algumas vezes substituindo o valor de uma por outra de mesmo nome. Aqui reside o perigo: se temos uma variável esperada via cookie, um atacante pode enviá-la via método GET, e o script nem perceber a diferença, se usar a variável global.

Portanto, é importante usar mecanismos que forcem a verificação da origem dos dados vindos do usuário.

No arquivo de configuração PHP.INI existem duas diretivas de controle que são usadas como parâmetros para que o interpretador faça o merge: gps_order e variables_order. Ambos parâmetros refletem a prioridade relativa das fontes de entrada de dados.

Não vamos entrar muito neste assunto, visto que já presumimos que usamos as variáveis superglobais ($_GET, $_POST, etc) para acessar tais dados. Entretanto, cabe uma orientação: mesmo usando as superglobais, existe um risco especialmente em casos em que criamos variáveis para verificar se um dado script foi carregado ou não (ou se tal arquivo pode ser incluído ou não) - nestes casos, como a variável pode estar exposta à configuração do servidor, cabe uma advertência: é melhor usar constantes (define) ao invés de usar uma variável simples. Isso porque a variável simples pode ser substituída por um parâmetro injetado por um atacante, se não estiver inicializada e dependendo da configuração do servidor.

Outra observação importante, é o uso da superglobal $_REQUEST - ela mistura as variáveis GET, POST e COOKIES em um mesmo array. É o mesmo problema da register globals, embora em menor grau. Portanto, prefira o uso de $_GET ou $_POST.

Conteúdo

É importante sempre verificar a natureza do parâmetro de entrada e validá-lo para evitar problemas depois.

Dados numéricos

Se o dado esperado é numérico, simplesmente usamos o recurso de casting para obter o valor. Veja os exemplos a seguir:

$id_produto = (int) $_GET['id_produto'];
$valor_salario = (float) $_POST['valor_salario'];

Para melhorar um pouco mais, podemos centralizar o casting onde de direito: nos getter e setters das classes:

class Funcionario {

private $valorSalario;


public function getValorSalario() {
return $this->valorSalario;
}

public setValorSalario($valor) {
$this->valorSalario = (float) $valor;
if (!$this->valorSalario) {
$e = new Exception('Valor do salario deve ser float',999);
throw $e;
}
}
}


Veja que no exemplo acima se ocorre um erro de tipo, uma excessão é imediatamente emitida e o programador já mata o problema na raiz. A abordagem acima pode ser melhorada incluindo na mensagem o valor que se está passando para o atributo em questão.

Um ponto que deve ser observado com cuidado é a precisão do tipo numérico que se espera. Por exemplo: para validar um inteiro, em sistemas de 32 bits, é 2,147,483,647. Se uma string "1000000000000000000" for convertida para integer, ela vai estourar o container e haverá perda de dados. No caso, o resultado final será truncado no máximo possível, que é 2147483647.
Então, fazendo o casting de números grandes na notação científica evitará a perda de dados:

echo (int)"100000000000000000"; // 2147483647
echo (float)"100000000000000000"; // float(1.0E+17)


Enquanto que o casting funciona bem para inteiros e números de ponto-flutuante, valores em hexadecimal, octal e descritos em notação científica não são tratados automaticamente - é necessário implementar mecanismos adicionais de validação. O mais lento mas mais eficaz é a instrução "is_numeric" - Ela suporta todos os tipos de valores numéricos, e retorna True se o valor é válido.


Problemas de internacionalização

Algumas vezes números de ponto-flutuante são representados de várias formas ao redor do mundo, mas tanto o casting quanto o "is_numeric" consideram números que não usam o "ponto" (.) como separador decimal como inválido.

O problema é que para a validação em si, não encontrei recursos nativos no PHP que consideram a entrada de dados em localizações (locales) diferentes. Os mecanismos de locales funcionam muito bem para exibir o resultado de valores no formato esperado na língua selecionada. Por isso, recomendo que se use rotinas que façam a conversão do conteúdo logo na entrada dos dados. Para o caso de dados númericos de ponto flutuante, eu uso:

$r = setlocale(LC_ALL, 'ptb');
$locale_info = localeconv();

$numero = str_replace($locale_info['thousands_sep'],"",$numero_input);
$numero = str_replace($locale_info['decimal_point'],".",$numero);

Com a lógica acima, o script "sabe" quais caracteres espera que sejam entrados pelo usuário como separador decimal e de milhar. Automaticamente o valor é convertido para o formato que o interpretador "entende", e daí para adiante o valor é processado e depois inserido/atualizado no banco de dados; Depois, a exibição do dado fica por conta do interpretador, que vai considerar o locale selecionado. No caso do exemplo acima, defini a localização como sendo Português Brasileiro.


Validação de String

A validação de string não é tão simples quanto a de um atributo numérico, uma vez que o casting não é suficiente para garantir que o valor entrado é o esperado - existem algumas variantes que precisam ser consideradas. O primeiro ponto é o formato do valor entrado, que pode ser um CPF, número de telefone, URL, CEP, etc.

Para verificar se uma string possui apenas caracteres alfabéticos, não sendo admitido qualquer número ou pontuação, pode-se usar a instrução 'ctype_alpha()'. A extensão CTYPE é habilitada por padrão no PHP. Existem outras instruções dessa extensão que podem ser úteis em questões bem particulares, mas nenhuma delas consiste regras de formação mais complexas. Veja ainda que a Ctype considera o locale ativo. Portanto, caracteres que são considerados inválidos no alfabeto inglês (como 'ç'), serão considerados válidos pela Ctype quando o locale PTB estiver setado.

Quando o ctype é inútil para resolver um determinado problema de validação, recomenda-se usar expressões regulares. Com expressões regulares é possível realizar uma série de verificações bem complexas, tanto de conteúdo quanto do posicionamento deste no corpo do texto, e também múltiplas ocorrências de padrões de conteúdo.
Para fazer tais verificações com o PHP, pode-se usar as instruções 'ereg' e seus semelhantes, bem como as instruções presentes na extensão PCRE, como a preg_match. Veja maiores detalhes do uso de cada uma destas opções no manual do PHP.


Validação de tamanho de conteúdo

Assim como dados numéricos, entradas de strings precisam atender certas especificações. As expressões regulares podem validar a sintaxe dos dados entrados, mas tabém é importante validar o tamanho da entrada.

Uma falha que pode ser explorada por um atacante, consiste na geração de um valor que excede o tamanho do campo no banco de dados, e com isso um erro de banco será emitido - se este erro não for tratado, informações valiosas para o atacante poderão ser dadas.

A solução deste problema possui duas partes: criar os formulários com mecanismos de verificação do conteúdo antes do envio dos dados ao servidor, e a verificação dos mesmos na entrada do script. Nos formulários os campos de textos podem ter tamanho máximo, com o atributo maxlength. Para campos de texto-livre (textarea) é necessário verificar o tamanho do conteúdo ao se enviar os dados, com funções javascript.


Validação de Listas de Seleção

Listas de valores onde o usuário pode selecionar um item (ou mesmo mais) é outro foco de atenção que o desenvolvedor deve ter. Veja que um atacante pode emular uma requisição injetando novos valores, que podem causar desde erros de processamento, passando por inconsistências, chegando a danificar parcialmente ou consideravelmente o sistema. Portanto, valores discretos como strings que podem ser usadas como chaves em listas de seleção, e até mesmo valores numéricos, devem ser verificados no lado do servidor antes de se prosseguir com o processamento da requisição.
Para realizar essa validação, recomenda-se o uso de uma lista de valores em um array, que será usado na verificação da consistência do valor informado.