Projetado para o Tomcat 6, o BadInputFilter fornece uma linha de frente de defesa contra falhas de segurança de aplicações web comuns, como ataques de injeção de SQL (SQL Injection) e cross-site scripting. Infelizmente, o BadInputFilter rompe o silêncio em implementações posteriores do Tomcat. Neste artigo, aprenda a restaurar os benefícios de segurança da BadInputFilter para todas as versões do Tomcat, e até mesmo para o uso em outros containers Servlet/JSP.

Alguns livros técnicos são mais úteis do que outros, mas um que eu é, de fato, extremamente útil é "Tomcat: The Definitive Guide (segunda edição)" por Jason Brittain e Ian F. Darwin. Mesmo este livro sendo escrito para o Tomcat 6, a maior parte ainda é aplicável para o Tomcat 7, atualmente a última versão estável.

Nesse artigo você pode ver como instalar e configurar o Apache Tomcat 7.

Brittain & Darwin fornecem uma introdução completa à configuração e execução do Apache Tomcat em ambientes de produção. O livro também fornece o código fonte para um par de classes úteis nomeadas BadInputFilter e BadInputValve, que podem ser utilizadas para filtrar os requests potencialmente perigosos. O problema é que estas duas classes confiam no conhecimento que é específico para o Tomcat 6. Nas versões mais recentes do Tomcat, BadInputValve não compila, e uma parte da funcionalidade fornecida pelo BadInputFilter não funciona mais. Tanto a versão original do BadInputFilter e uma versão atualizada disponível no SourceForge tem esse problema. Além disso, a falha de BadInputFilter está em silêncio, no sentido de que não há nenhuma indicação de que ele já não faz parte do seu trabalho.

Neste artigo será proposta uma atualização e correção da BadInputFilter. Para limitar o escopo do artigo, concentremos na implementação original da BadInputFilter do livro Brittain e Darwin. Você pode usar uma abordagem similar para a implementação atualizada com algumas mudanças que são bastante simples. Na verdade, você vai encontrar duas versões do BadInputFilter atualizada no código-fonte que acompanha este artigo. No pacote com.devmedia.filter, o BadInputFilter corresponde à implementação original e o BadInputFilter2 corresponde à implementação atualizada atualmente hospedada no SourceForge.

Presumiremos que você tenha programado em Java e tem alguma familiaridade com o uso de tecnologia baseada em Java para aplicações web, como Servlets, JSPs, e assim por diante. Presumo também que você saiba o papel que desempenha no Tomcat esses tipos de aplicações. Forneceremos uma breve introdução aos filtros, a fim de dar o contexto para o restante do artigo.

Filtros e Válvulas

Então, o que é um filtro? Conforme descrito no Javadoc para a interface Filter, um filtro é um objeto que faz as tarefas de filtragem quer sobre o pedido de um recurso (por exemplo, um servlet ou um arquivo html), quer na resposta de um recurso ou ambos. Mais de um filtro pode ser aplicado em uma cadeia com um recurso ou uma coleção de recursos, que é um excelente exemplo da cadeia de padrão de Responsabilidade. A Figura 1 ilustra o uso de filtros em uma aplicação web.

Filtros em uma aplicação Java Web

Figura 1. Filtros em uma aplicação Java Web

Exemplos de usos para filtros incluem:

  • autenticação;
  • Logging e auditoria;
  • compressão de dados;
  • criptografia.

Uma das coisas legais sobre filtros é que eles podem ser adicionados a uma aplicação após o fato. Por exemplo, suponha que você tenha uma aplicação web existente, e você decide que deseja registrar algumas mensagens especiais sempre que os pedidos são recebidos por certos recursos da web. Você pode reescrever a parte do aplicativo que serve esses recursos, ou você pode simplesmente escrever um filtro que intercepta pedidos de recursos, registra a mensagem e, em seguida, encaminha o pedido para ser tratado da forma normal. Na segunda abordagem a aplicação web existente permanece sem modificações exceto o descritor de implantação da aplicação web, um arquivo XML denominado web.xml onde especifica a existência do filtro e os recursos para que aplica.

Válvulas são semelhantes aos filtros com exceção de três diferenças muito importantes:

  1. Válvulas são específicas para o Tomcat e, portanto, executam apenas no container Servlet/JSP Tomcat. Filtros, por outro lado, são parte da especificação Java Servlet e são projetados para serem independentes de qualquer container Servlet/JSP, por isso eles são portáteis para outros containers Servlet/JSP, além de Tomcat. Infelizmente, o código para BadInputFilter depende de detalhes da implementação em Tomcat 6, o que mudou com as versões mais recentes do Tomcat.
  2. Se sua arquitetura é composta por várias aplicações web no mesmo servidor Tomcat, as válvulas podem ser configuradas em um só lugar para filtrar pedidos de todos ou de alguns deles. Os filtros devem ser configurados separadamente para cada aplicativo web.
  3. Os filtros são facilmente configurados para rodar em padrões de URL específicos. Válvulas exigem que você escreva seu próprio código correspondente a esta finalidade.

Usando BadInputFilter para segurança de aplicações web

Se uma aplicação web não é projetada com segurança como um requisito em mente, é provável que esteja vulnerável a ataques de segurança externos, cross-site scripting (XSS), injeção de HTML, e de injeção de SQL. O BadInputFilter foi projetado para verificar a entrada do usuário potencialmente perigoso e filtrar os bad requests. Embora você provavelmente deva tomar medidas de segurança adicionais, você pode pensar em BadInputFilter como uma primeira linha de defesa para algumas falhas de segurança de aplicações web bem conhecidas.

Essencialmente, o BadInputFilter analisa solicitações de usuários para possíveis problemas. Se um problema é encontrado, ele executa uma das duas ações - ou (1) que proíbe a solicitação ou (2) que escapa a entrada do usuário ruim. A execução dessas ações em um filtro permite-lhe implementar o código uma vez e, em seguida, aplicá-lo facilmente a vários (ou todos) os recursos dentro do mesmo aplicativo web.

Proibindo a entrada do usuário pode ser realizado enviando um código de status de resposta HTTP 403 (proibido) ao cliente. Por exemplo, se a entrada do usuário contém certos caracteres de controle não-imprimíveis, você provavelmente vai querer negar a solicitação por completo. A parte de BadInputFilter que proíbe determinados caracteres dentro da entrada do usuário continuará a funcionar corretamente. Mas a segunda parte, a parte que escapa a entrada do usuário ruim, irá falhar.

Onde o BadInputFilter falha

Uma série de falhas de segurança web envolvem a introdução de caracteres ou frases que têm significado especial dentro do contexto de HTML, JavaScript, ou SQL. Por exemplo, se a entrada do usuário não está devidamente validada ou escapou, é possível entrar no JavaScript que é executado no servidor ou que revela informações sobre o ambiente do servidor. Como um exemplo disso, escapando a entrada do usuário, você pode querer olhar para todas as ocorrências de suportes de ângulo esquerdo ou direito ("<" ou ">") e substituí-los por entidades HTML/XML equivalentes ("<" ou ">", respectivamente). Assim, a entrada do usuário contendo (indicando possível código JavaScript) seria inofensiva, na maioria dos casos, mas que ainda apresentam adequadamente se ecoou de volta para o usuário como parte de uma página de resposta HTML.

Em seu livro, Darwin Brittain fornece uma página JSP de teste (input_test.jsp) que pode ser usada para ver como as várias entradas do usuário filtradas e não filtradas são tratadas. Configurando o BadInputFilter para filtrar os pedidos para esta página teste mostra que BadInputFilter já não escapa a entrada do usuário como inicialmente previsto. A razão que o BadInputFilter falha é que ele depende de detalhes de implementação do Tomcat 6.

Aqui está uma breve descrição, a partir de uma perspectiva de implementação, do por que o BadInputFilter falhou. Filtros têm acesso à entrada do usuário através de um objeto do tipo HttpServletRequest. Para um objeto HttpServletRequest, o método getParameterMap() deve retornar um Map imutável contendo nomes e valores dos parâmetros. No Tomcat 6, a imutabilidade do objeto poderia ser contornada por meio de reflexão para obter acesso ao método setLocked() e , em seguida, utilizando-o com um parâmetro de false, simplemente desbloqueia o imutável mapa. Com Map desbloqueado, foi possível alterar os nomes e/ou valores de parâmetro. Em versões posteriores do Tomcat, usando essa abordagem para desbloquear e modificar os parâmetros de solicitação não funcionaria, mas o fracasso é silencioso. Não há indicação de falha aparente em qualquer lugar do sistema, nem em uma página web ou em um arquivo de log. A entrada do usuário é simplesmente repassada inalterada - significando que não é escapado.

Modificando o BadInputFilter para escapar entrada do usuário

Como já descrito, HttpServletRequest tem um método chamado getParameterMap() que retorna um Map imutável, mapeando assim os nomes dos parâmetros para valores. Mas, a fim de "escapar" a entrada do usuário, você precisa ser capaz de modificar esses nomes e/ou valores de parâmetro. A questão é como modificar um Mapa imutável. Obviamente, você não pode modificar o Mapa diretamente (ou , pelo menos, você não deve ser capaz de modificar o mapa), mas você pode envolver todo o HttpServletRequest em um HttpServletRequestWrapper, que pode ser repassado para o recurso de destino no lugar do request original. O request "embrulhado" (wrapped) copia e escapa os nomes dos parâmetros originais e valores, que o recurso de destino, em seguida, tem acesso.

Vejamos o código para corrigir os problemas existentes com o BadInputFilter na forma de duas classes Java. A primeira classe, FilterableRequest, estende HttpServletRequestWrapper, a fim de proporcionar o acesso e modificação (escapar) dos parâmetros de solicitação. O construtor do FilterableRequest faz uma cópia local do parâmetro Map do HttpServletRequest que o encapsula. Em seguida, ele substitui os métodos herdados que dão acesso ao Map para que eles usem a cópia local. Métodos herdados ainda tratam o parâmetro Map como imutável, mas o FilterableRequest também fornece um método novo, getModifiableParameterMap(), que permite a modificação dos parâmetros. A Listagem 1 é o código fonte necessário para construir o FilterableRequest.

Listagem 1. Criação da FilterableRequest

package com.devmedia.filter;
   
  import java.util.*;
  import javax.servlet.http.*;
   
  /**
  * Quebra um HttpServletRequest de modo que os parâmetros podem ser modificados por um filtro.
  *
  * @author John I. Moore, Jr.
  */
  public class FilterableRequest extends HttpServletRequestWrapper {
     private Map<String, String[]> parameters = null;
   
     /**
       * Constrói um wrapper para o pedido original.
       *
       * @param request HttpServletRequest o original
       */
     public FilterableRequest(HttpServletRequest request) {
          super(request);
          parameters = new TreeMap<String, String[]>();
          parameters.putAll(super.getParameterMap());
       }
   
     @Override
     public String getParameter(final String name) {
          String[] values = parameters.get(name);
          return values != null ? values[0] : null;
       }
   
     @Override
     public Map<String, String[]> getParameterMap() {
          return Collections.unmodifiableMap(parameters);
       }
   
     /**
       * Retorna um ParameterMap que pode ser modificado.
       */
     protected Map<String, String[]> getModifiableParameterMap() {
          return parameters;
       }
   
     @Override
     public Enumeration getParameterNames() {
          return Collections.enumeration(parameters.keySet());
       }
   
     @Override
     public String[] getParameterValues(final String name) {
          return parameters.get(name);
       }
  }

Considerando que partes da implementação de BadInputFilter ainda funcionam corretamente (por exemplo, o código de inicialização e a parte que proíbe a entrada do usuário através do envio de um código de status de resposta HTTP de 403), a maneira mais fácil para ilustrar a correção para a versão do Jason Brittain de BadInputFilter é criar uma nova versão, que se estende à versão original e, em seguida, substitui apenas os métodos que precisam mudar. (Esta abordagem é conhecida como herança de implementação.).

O código-fonte para a nova versão do BadInputFilter é mostrado na Listagem 2. A essência das mudanças pode ser resumida em:

  • No método doFilter(), criamos um FilterableRequest que envolve o parâmetro HttpServletRequest e depois passa este FilterableRequest ao método filterParameters da seguinte forma:
    FilterableRequest filterableRequest = new FilterableRequest((HttpServletRequest) request);
    filterParameters(filterableRequest);
  • No método filterParameters(), chamamos o getModifiableParameterMap() sobre o pedido filtrável para ter acesso aos parâmetros em vez de tentar "destravar" o Mapa imutável usando reflexão.

Essencialmente, o resto do código nestes dois métodos é modificado apenas ligeiramente a partir da implementação do BadInputFilter original. A Listagem 2 mostra o código fonte para estes dois métodos na nossa versão de BadInputFilter.

Listagem 2. Atualizando o BadInputFilter

@Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
  // Filtrar as solicitações e respostas não-HTTP.
  if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
     filterChain.doFilter(request, response);
     return;
   }
   
  // Só permitir requests baseados no "permite e nega".
  if (processAllowsAndDenies(request, response)) {
     /* Filtrar a entrada para um código JavaScript potencialmente perigoso para que a entrada do usuário ruim seja limpa fora do pedido no momento em que o Tomcat começar a executar o pedido. */
     FilterableRequest filterableRequest = new FilterableRequest((HttpServletRequest) request);
     filterParameters(filterableRequest);
   
     // Inicia a requisição.
     filterChain.doFilter(filterableRequest, response);
   }
  }
   
  /**
  * Filtra todos os parâmetros existentes para conteúdo potencialmente perigoso, 
    e escapa a qualquer um se forem encontrados.
  *
  * @param request O FilterableRequest que contém os parâmetros
  */
  public void filterParameters(FilterableRequest request) {
  Map<String, String[]> paramMap = request.getModifiableParameterMap();
   
  // Passa através de cada um dos padrões de substituição.
  for (String patternString : parameterEscapes.keySet()) {
     Pattern pattern = Pattern.compile(patternString);
   
     // Percorre a lista de nomes de parâmetros.
     for (String name : paramMap.keySet()) {
          String[] values = request.getParameterValues(name);
   
          // Vê se o nome contém o padrão.
          boolean nameMatch;
          Matcher matcher = pattern.matcher(name);
          nameMatch = matcher.find();
          if (nameMatch) {
               /* O nome do parâmetro corresponde a um padrão, por isso corrija-o, modificando o nome, adicionando o parâmetro de volta como o novo nome, e removendo o antigo. */
               String newName = matcher.replaceAll((String) parameterEscapes.get(patternString));
               paramMap.remove(name);
               paramMap.put(newName, values);
               servletContext.log("Nome do parâmetros " + name
                     + " casou com padrão \"" + patternString
                     + "\”. Emdereço remoto: "
                     + ((HttpServletRequest) request).getRemoteAddr());
             }
        }
   
     // Percorre a lista de valores de parâmetros para cada nome.
     for (String name : paramMap.keySet()) {
          String[] values = request.getParameterValues(name);
          // Verifique os valores do parâmetro para o padrão.
          if (values != null) {
               for (int j = 0; j < values.length; j++) {
                     String value = values[j];
                     boolean valueMatch;
                     Matcher matcher = pattern.matcher(value);
                     valueMatch = matcher.find();
                     if (valueMatch) {
                          // O valor verificado, então nós modificamos o valor e setamos ele de volta no vetor
                          String newValue;
                          newValue = matcher.replaceAll((String)
                               parameterEscapes.get(patternString));
                          values[j] = newValue;
                          servletContext.log("Parâmetro \"" + name
                               + "\” valor \"" + value
                               + "\” padrão usado \""
                               + patternString + "\”. Endereço remoto: "
                               + ((HttpServletRequest) request).getRemoteAddr());
                          servletContext.log("newValue =” + newValue);
                        }
                   }
             }
        }
   }
  }

Configurando o BadInputFilter

O livro de Brittain & Darwin contém definições de configuração de amostra para o BadInputFilter que podem ser adicionadas para o descritor de implementação de aplicações web (web.xml). A Listagem 3 mostra o código de configuração de exemplo dado no livro.

Listagem 3. Exemplo de configuração para o BadInputFilter

<filter>
       <filter-name>BadInputFilter</filter-name>
       <filter-class>com.oreilly.tomcat.filter.BadInputFilter</filter-class>
       <init-param>
         <param-name>deny</param-name>
         <param-value>\x00,\x04,\x08,\x0a,\x0d</param-value>
       </init-param>
       <init-param>
         <param-name>escapeQuotes</param-name>
         <param-value>true</param-value>
       </init-param>
       <init-param>
         <param-name>escapeAngleBrackets</param-name>
         <param-value>true</param-value>
       </init-param>
       <init-param>
         <param-name>escapeJavaScript</param-name>
         <param-value>true</param-value>
       </init-param>
     </filter>
     <filter-mapping>
       <filter-name>BadInputFilter</filter-name>
       <url-pattern>/input_test.jsp</url-pattern>
     </filter-mapping>

A mudança mínima necessária para usar a nossa versão do BadInputFilter é substituir com.oreilly.tomcat.filter.BadInputFilter por com.devmedia.filter.BadInputFilter no elemento desta configuração. Além disso, vale a pena considerar algumas outras mudanças possíveis para tanto o código de configuração da Listagem 3 ou para o código-fonte original de BadInputFilter.

Primeiro, o Eclipse Kepler (4.3.1) e o Java 7 vão dar vários avisos relacionados com os genéricos ao compilar a versão original do BadInputFilter. Isto não é surpreendente, porque o código original foi escrito quando os genéricos eram relativamente novos para o Java. Em um ponto no código original de Jason Brittain há uma anotação SuppressWarnings para se livrar do tal aviso, mas outros avisos existem agora. Existem várias maneiras de se livrar desses avisos, mas seguindo o código de Jason podemos simplesmente adicionar as anotações SuppressWarnings da seguinte forma:

  1. Adicione @SuppressWarnings("rawtypes") antes de método processAllowsAndDenies().
  2. Substitua @SuppressWarnings("unchecked") por @SuppressWarnings ({"não verificadas", "rawtypes"}) antes do método filterParameters.
  3. Remover @SuppressWarnings("unchecked") a partir do meio do método filterParameters() desde que a mudança no item 2 acima se torne redundante.

Se você desenvolver e/ou manter um site que é aberto ao público, você deve antecipar que alguém vai tentar invadi-lo em algum ponto no tempo. Uma maneira de reduzir a vulnerabilidade de seu site é através da validação de todas as entradas do usuário. A classe BadInputFilter foi originalmente projetado para fornecer uma linha de frente de defesa para várias falhas de segurança de aplicações web bem conhecidos por pedidos de filtragem e que proíbe o pedido ou escapa a entrada potencialmente maliciosos. Com as versões mais recentes do Tomcat, uma parte da funcionalidade fornecida pelo BadInputFilter não funciona mais. Neste artigo vimos algumas mudanças para o BadInputFilter que irá restaurar a funcionalidade perdida. Além disso, ao contrário da versão original do BadInputFilter, a versão atualizada não depende da implementação do Tomcat e deve funcionar corretamente em qualquer container Servlet/JSP.