DevSecOps

30 mai, 2012

Os pecados capitais da validação de dados de entrada

Publicidade

O aspecto mais importante na garantia da segurança de uma aplicação é uma correta validação dos dados de entrada. Oito dos dez principais ataques a aplicações envolvem, de uma forma ou de outra, a possibilidade de o atacante passar informações inválidas para o sistema, sem que este faça a crítica adequada.

Mas isso é de conhecimento de todos. O que me intriga profundamente é que esse problema não deveria existir. Veja a seguir por que não:

  • A validação de dados de entrada é um requisito presente em todos os sistemas, desde os primórdios da computação. Assim, todo desenvolvedor e codificador deveria conhecer o assunto;
  • Todas as plataformas contam com funções e mecanismos que ajudam o desenvolvedor a fazer uma  validação correta dos dados de entrada. O Clipper tinha isso numa época em que a maior parte dos desenvolvedores de hoje não sabia ler e escrever ainda;
  • As plataformas mais modernas, como Java, C# e outras, incluem dezenas de mecanismos para validação de dados de entrada. Alguns já prontos em frameworks;
  • Qualquer manual de boas práticas de codificação vai incluir a validação de dados de entrada entre os aspectos principais. Porque não se trata apenas de segurança, mas também da qualidade do código como um todo.

Ainda assim, vemos falhas de segurança devido à falta de validação de dados de entrada. Buffer overflow, SQL Injection, Code Injection e diversos outros ataques estão ainda entre os mais efetivos. Quais são os pecados que os desenvolvedores cometem para abrir a guarda a esses ataques? Vamos ver cinco dos principais erros cometidos. Mas não são os únicos.

Pecado 1: Testar os dados de entrada em código acessível ao usuário

Isso quer dizer que toda validação de dados de entrada deve ser feita no servidor, nunca no cliente rico e muito menos no código javascript.

Se você é um desenvolvedor com alguma preocupação em segurança, vai pensar logo: não fazer validação em Javascript? Isso é óbvio. Sim, eu concordo. Mas, nas análises de segurança que faço, ainda encontro muita aplicação web que confia em rotinas em JavaScript para validar dados de entrada. Confia no atributo maxlength da tag input para delimitar o tamanho máximo do campo.

Se você ainda não sabia, fique sabendo. Existem várias ferramentas e até plug-ins para browsers que permitem remover qualquer tipo de validação que seja feita em linguagem script no browser. Veja, não há nada errado em fazer essa validação em JavaScript, desde que a mesma seja feita novamente no servidor. Muitas vezes, queremos evitar que o usuário cometa um erro na digitação, então a validação em JavaScript está ok. O erro honesto será pego. O problema é que não pode ser a única validação, posto que o usuário mal intencionado, ou seja, o atacante, pode contornar essas validações de forma bastante simples.

Mas a questão aqui não se restringe aos sistemas web. Os aplicativos ricos, como cliente-servidor ou sistemas para dispositivos móveis, podem também ser burlados por engenharia reversa. Se você tem um cliente rico que faz a validação de dados de entrada, é simples para um atacante, com as ferramentas corretas e algum conhecimento de assembler, anular essa validação ou contorná-la.

Muita gente acha impraticável fazer ataques de engenharia reversa em assembler. Mas acredite: o assembler de qualquer processador é muito fácil de aprender. Embora seja virtualmente impossível escrever um sistema em assembler com a complexidade dos sistemas a que estamos habituados, ler o código em assembler e entender o que ele está fazendo não é tão difícil assim. Se for em código intermediário Java ou .NET então, mais simples ainda, afinal, existem dezenas de compiladores reversos. Uma vez identificado o ponto em que existe a validação, basta inverter o opcode em assembler, ou usar o famoso NOP para eliminar a validação.

Assim, a recomendação é clara: teste sempre no servidor. Você pode optar por testar no cliente também, dando uma resposta mais graciosa ao usuário, mas o teste no servidor, em código fora do alcance do usuário, é fundamental.

Pecado 2: Assumir dados pré-definidos

Listas pré-definidas não necessariamente garantem a resposta dentre os valores delimitados. O caso mais comum é o dos objetos HTML <selection> <option>. Neles, você cria uma lista de opções e o usuário deve escolher uma delas. É a versão web do Combo Box, que aparece também nas plataformas de maior nível, como J2EE, .NET e outras. Nesses casos, você monta uma lista de opções e o usuário terá que retornar uma daquelas opções. Certo? Errado! O usuário pode retornar no campo indicando qualquer coisa, inclusive de tipos diferente, como uma string ao invés de um inteiro.

Isso ocorre porque no protocolo http todo o campo de formulário é algo sem tipo definido. O browser normal irá retornar uma das opções, mas basta o plug-in correto ou o uso de uma ferramenta como o WebScarab para alterar a informação por uma string de ataque.

O mesmo problema ocorre em dados que são originalmente criados pelo próprio sistema, por exemplo, em campos hidden ou em parâmetros passados via GET. O fato de o seu sistema ter criado os dados originalmente não garante que os mesmos não serão alterados pelo usuário mal intencionado. Qualquer informação que trafegue pela máquina do cliente pode ser alterada por ele. Isso também vale para dados temporários armazenados em dispositivos móveis. A solução? Validar novamente os dados ao recebê-los. Sua combo box continha apenas inteiros na montagem da lista? Verifique se o resultado retornado é um inteiro e se estava na lista original.

Pecado 3: Assumir que a interface é confiável

Algumas situações me surpreendem particularmente quando uma falha simples compromete a segurança de um sistema bastante robusto. Certa análise que fiz em um sistema de comércio eletrônico mostrou que houve uma preocupação grande dos desenvolvedores com a validação dos dados. Todos os dados eram validados de forma correta em todo o site. Porém, ainda assim, consegui identificar um ponto vulnerável para a SQL Injection que comprometia todo o sistema. Esse ponto estava na interface de recebimento de informações do gateway de pagamento. Após a compra, o sistema passava o controle para um gateway de pagamento que, ao final, retornava em uma página específica uma confirmação da conclusão do pagamento. O desenvolvedor, assumindo que as informações enviadas pelo gateway de pagamento estariam num determinado padrão, usava as mesmas diretamente, sem validação adequada.

O problema é simples: eu poderia me fazer passar pelo gateway de pagamento enviando informação com um SQL Injection. Repare que, mesmo que a interface não tenha sido projetada para receber dados do usuário, ela pode ser usada assim. Nesse caso de interface entre sistemas, é bom notar que seria de bom tom também ter a autenticação mútua entre os sistemas, o que impediria um ataque.

Como outros exemplos, posso citar dezenas de WebServices Ajax que não se preocupavam em validar dados de entrada, posto que assumiam que seriam sempre chamados por determinada rotina do próprio sistema. Mesmo que houvesse autenticação aqui, eu conseguiria acessar o WebService Ajax, já que o mesmo seria autenticado na minha própria página.

Não se pode confiar em nenhuma interface. Toda função que recebe dados de fora do servidor deve validar todos os dados de entrada.

Pecado 4: Validação não uniforme

A forma usada para validar varia muito conforme o framework usado. Alguns sistemas já disponibilizam toda uma infraestrutura para validação dos dados, ao passo que, em outras linguagens, tal validação deve ser feita pelo desenvolvedor quase que exclusivamente. Um dos riscos que se corre é programar a validação em cada ponto do código, de forma independente. O problema disso é que sistemas evoluem, mudam com o tempo. Na manutenção, pode ocorrer uma mudança que torne a função de validação inconsistente.

Caso clássico disso é a validação de tamanho do campo. O campo teve seu tamanho diminuído posteriormente, mas a validação continua a checar pelo tamanho original, abrindo possibilidade de um buffer overflow. Para evitar esse tipo de problema, a recomendação é que seja usado um padrão uniforme de validação de dados de entrada.

Isso não requer uma função única, mas requer que exista um padrão. Por exemplo, uma função para validar nomes de pessoas, outra para telefones, outra para CNPJ e assim por diante. Todas elas encapsuladas em uma unidade, módulo ou classe do sistema. Isso garante uniformidade no tratamento e evita que se deixe de testar todos os três aspectos críticos da validação de dados de entrada:

  • O dado imputado tem o tamanho adequado?
  • O dado imputado tem o formato adequado?
  • O dado imputado contém apenas caracteres esperados?

Pecado 5: Validar por exclusão

Uma cadeia de caracteres, ou string, é o tipo de informação mais sensível no quesito validação, pois admite diversos caracteres em um número não definido a principio. Justamente nas strings estão os principais problemas de segurança.

Tipicamente, numa string são verificados o seu tamanho, que deve estar de acordo com o esperado, e se ela contém algum caractere problemático. Por exemplo, se contem um plic (‘) ou um sinal de menor (<), que pode gerar SQL ou code Injection. Embora essa abordagem garanta a eficácia para determinados ataques, pode falhar para outros que não sejam de conhecimento do desenvolvedor ou até para ataques que venham a surgir posteriormente.

Assim, a recomendação é que a validação seja feita ao contrário: seja definido um conjunto de caracteres permitidos e todos os demais devem ser negados. Mais do que isso, é importante verificar para cada campo string qual conjunto mínimo de caracteres seria necessário naquele campo e se limitar a eles.
Por exemplo, nomes de pessoas podem ter letras, espaços e hífen. Não existem nomes pessoais com dígitos ou pontos. Endereços, por outro lado, podem requerer ainda a vírgula, o ponto, a barra e os dígitos.

Quando esses dados serão usados em redes celulares ou em sistemas de grande porte, é conveniente não incluir sequer caracteres acentuados, visto que as conversões nesses sistemas são ainda sujeitas a erros e podem levar a brechas de segurança. Esse mecanismo preventivo visa garantir que só caracteres mínimos, necessários e seguros sejam aceitos, impedindo os ataques atuais e eventuais problemas que venham a surgir no futuro.

Dilema do defensor

Como podemos ver, a validação de dados de entrada reflete de forma clara o dilema do defensor. Temos que validar todos os dados, em todas as situações, todo o tempo e da forma mais estrita possível. O atacante tem a tarefa muito mais simples: basta encontrar um ponto com uma falha. Mas a validação de forma efetiva é o principal aliado do desenvolvedor para a garantia da segurança. Uma validação de dados de entrada bem feita é metade do caminho andado para uma aplicação segura.