Novos recursos do Java 17 para aprimorar a segurança e a serialização
21 de outubro de 2021
0 minutos de leituraEm dezembro de 2020, escrevi o artigo Serialização e desserialização em Java: explicando a vulnerabilidade de desserialização do Java sobre os problemas da implementação personalizada de serialização do Java. O framework de serialização está tão profundamente integrado ao Java que é importante entender os riscos de algumas implementações. A desserialização insegura pode levar à execução de código arbitrário quando uma cadeia de gadgets é criada a partir das classes no classpath.
Recentemente, foi lançado o Java 17, a nova versão LTS. Mas como os novos recursos afetam esse problema? E será que podemos prevenir melhor as vulnerabilidades de desserialização com eles?
Neste artigo, vou analisar três dos principais recursos do Java 17:
Records
Melhorias no Java Flight Recorder (JFR)
JEP 415 (Proposta de aprimoramento do Java): filtros de desserialização específicos para cada contexto.
1. Records
Records foram introduzidos no Java 14 como um recurso de prévia e disponibilizados oficialmente no Java 16. No entanto, como muitos desenvolvedores preferem atualizar apenas para versões LTS, faz sentido falar sobre Records no contexto da serialização agora que o Java 17 foi lançado oficialmente.
Ao contrário do que acontece com POJOs comuns, na desserialização de um Record, o construtor é usado para recriar o objeto. Isso não ocorre com objetos Java comuns, e o framework depende bastante de reflexão. Isso significa que qualquer lógica acionada pelo construtor não será executada durante a desserialização de um objeto Java comum. Leia meu artigo anterior para saber mais. No caso dos Records, não há nenhum mecanismo especial envolvido na recriação do objeto durante a desserialização. Portanto, se você incluir lógica de validação no construtor de um Record, sabemos que ela será aplicada.
Ainda assim, podemos discutir se você deveria incluir lógica em um record. Fazer isso de forma inadequada pode criar gadgets que participam de uma cadeia de gadgets de desserialização. Mais importante: ainda usamos a função readObject() para desserializar. Isso significa que os POJOs comuns continuam vulneráveis a cadeias de gadgets, independentemente dos records.
2. Filtros de desserialização no Java
Para lidar com as vulnerabilidades de desserialização em Java, é possível configurar filtros de serialização. Esse recurso foi introduzido no Java 9 com a implementação da JEP 290. Você pode definir limites para o tamanho de arrays, a profundidade do grafo, o total de referências e o tamanho do fluxo. Além disso, pode criar listas de bloqueio e de permissão com base em padrões para limitar as classes que podem ser desserializadas.
Você pode configurar um filtro global para a JVM ou um filtro individual para cada fluxo. Para o filtro global, é possível definir um argumento da JVM ou configurá-lo no código. No exemplo abaixo, criei um filtro que permite todas as classes de mypackage e bloqueia as demais.
Argumento da JVM:
Código:
Você também pode configurar um filtro para um fluxo específico, como no exemplo abaixo.
Até o Java 17, quando eu configurava um filtro para um fluxo específico, o filtro global era substituído naquele fluxo. Os filtros global e específico do fluxo não eram combinados. Essa não é uma forma muito flexível de trabalhar. Além disso, isso cria o problema de que o filtro global talvez não funcione quando uma biblioteca incluída por você faz a desserialização.
Filtros de desserialização específicos para cada contexto no Java 17
O Java 17 aprimorou o filtro de desserialização com a implementação da JEP 415.
Uma das ações mais importantes que você pode realizar agora é configurar uma fábrica SerialFilterFactory em ObjectInputFilter.Config. Essa fábrica deve ser um BinaryOperator e define o que fazer quando um filtro específico é adicionado a determinado fluxo.
No exemplo abaixo, configurei uma fábrica básica em Config que usa o método padrão de combinação para unir o filtro existente ao novo. Com esse recurso, posso decidir se os filtros devem ser combinados e de que maneira. Isso resolve o problema que mencionei antes sobre como as bibliotecas lidam com o filtro global.
Além da fábrica de filtros, o Java 17 também oferece métodos práticos para facilitar a criação de filtros. Na minha opinião, funções como allowFilter() e rejectFilter() de ObjectInputFilter tornam a criação de filtros mais declarativa e legível.
No exemplo de código abaixo, escrito em Java 17, uso esses novos recursos. No método de desserialização, rejeito especificamente a classe Gadget. Tanto a classe Gadget quanto o record TwoValue pertencem ao mesmo pacote. Com isso, Gadget será rejeitada, e o filtro permitirá todas as outras classes desse pacote.
3. Eventos de desserialização no Java Flight Recorder
O lançamento do Java 17 também trouxe uma boa adição ao Java Flight Recorder (JFR) para ajudar você a combater exploits de desserialização. Essa nova versão do Java oferece suporte a um evento específico para monitorar a desserialização. Um evento de desserialização é criado para cada objeto em um fluxo e registra várias informações interessantes, como o tipo do objeto, se havia um filtro, se o objeto foi filtrado, a profundidade do objeto, o número de referências etc.
Todas essas informações ajudam a identificar se há desserialização em algum ponto da sua aplicação e o que está sendo desserializado enquanto o processo é executado. No entanto, você precisa habilitar esse evento. Ele não é capturado por padrão, então é necessário configurar algo como o exemplo abaixo.
Vamos usar o exemplo de código anterior, mas agora desserializando a classe Gadget. Ao executar o código no IntelliJ IDEA com o Java Flight Recorder e a configuração personalizada, obtenho o seguinte resultado.

Você pode ver que o evento captura o tipo real do objeto durante a desserialização e indica que o filtro o rejeitou. Para saber mais sobre como usar esse evento específico de desserialização no JFR, confira o artigo Monitoramento da desserialização para aprimorar a segurança de aplicações, de Chris Hegarty.
Atualize para o Java 17 e tenha ferramentas mais poderosas contra exploits de desserialização insegura
A versão LTS do Java 17 traz melhorias significativas para prevenir a desserialização maliciosa nas suas aplicações Java. Por isso, atualizar para uma versão mais recente, como a 17, é essencial se você quiser adotar essas práticas. Ainda assim, na minha opinião, é melhor evitar completamente a serialização personalizada do Java. Porém, se você precisar usá-la ou depender de uma biblioteca que execute a desserialização personalizada do Java, agora sabe como se proteger.
Além disso, tome cuidado para não importar bibliotecas que contenham cadeias de gadgets de desserialização conhecidas ou apresentem outros problemas de segurança relacionados à desserialização.
Comece a jogar Capture the Flag
Aprenda a resolver desafios de Capture the Flag assistindo sob demanda ao nosso workshop virtual introdutório.



