Skip to main content

Novos recursos do Java 17 para aprimorar a segurança e a serialização

Escrito por
Blog Feature Java deserialize

21 de outubro de 2021

0 minutos de leitura

Em 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:

  1. Records

  2. Melhorias no Java Flight Recorder (JFR)

  3. 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:

-Djdk.serialFilter=nl.brianvermeer.example.*;!*

Código:

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("nl.brianvermeer.example.*;!*");
ObjectInputFilter.Config.setSerialFilter(filter);

Você também pode configurar um filtro para um fluxo específico, como no exemplo abaixo.

ObjectInputStream in = new ObjectInputStream(fileIn);
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("nl.brianvermeer.example2.Object;!*");
in.setObjectInputFilter(filesOnlyFilter);

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.

ObjectInputFilter.Config.setSerialFilterFactory((f1, f2) -> ObjectInputFilter.merge(f2,f1));

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.

public static void main(String[] args) throws IOException, ClassNotFoundException {
   var filename = "file.ser";
   var value = new TwoValue("one", "two");
   //var value = new Gadget(new Command("ls -l")); //This will not be deserialized

   var filter1 = ObjectInputFilter.allowFilter(cl -> cl.getPackageName().contentEquals("nl.brianvermeer.example.serialize.records"), ObjectInputFilter.Status.REJECTED);
   ObjectInputFilter.Config.setSerialFilter(filter1);
   ObjectInputFilter.Config.setSerialFilterFactory((f1, f2) -> ObjectInputFilter.merge(f2,f1));

   serialize(value, filename);
   deserialize(filename);
}

public static void serialize(Object value, String filename) throws IOException {
   System.out.println("---serialize");
   FileOutputStream fileOut = new FileOutputStream(filename);
   ObjectOutputStream out = new ObjectOutputStream(fileOut);
   out.writeObject(value);
   out.close();
   fileOut.close();
}

public static void deserialize(String filename) throws IOException, ClassNotFoundException {
   System.out.println("---deserialize");
   FileInputStream fileIn = new FileInputStream(filename);
   ObjectInputStream in = new ObjectInputStream(fileIn);
   ObjectInputFilter intFilter = ObjectInputFilter.rejectFilter(cl -> cl.equals(Gadget.class), ObjectInputFilter.Status.UNDECIDED);
   in.setObjectInputFilter(intFilter);
   TwoValue tv = (TwoValue) in.readObject();
   System.out.println(tv);

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.

<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0" description="test">
   <event name="jdk.Deserialization">
      <setting name="enabled">true</setting>
      <setting name="stackTrace">false</setting>
   </event>
</configuration>

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.

Visualização de eventos do profiler do Java mostrando um evento de desserialização rejeitado e suas propriedades, incluindo tipo, bytes lidos e status do filtro.

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.

Leia mais

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

Blog

Por que agentes de programação com IA continuam criando falhas de controle de acesso

Agentes de programação com IA podem gerar uma lógica de autorização que compila e passa pela revisão, mas permite que um tenant acesse os dados de outro. Saiba por que é difícil detectar falhas de controle de acesso e como evitá-las.