Skip to main content

Problemas de desserialização de JSON em Java com o Jackson ObjectMapper

Escrito por
Blog Feature Java deserialize

1 de dezembro de 2021

0 minutos de leitura

Em um artigo anterior, analisamos a plataforma de serialização personalizada do Java e suas implicações para a segurança. Mais recentemente, escrevi sobre como as melhorias do Java 17 podem ajudar você a evitar a desserialização insegura. Hoje, porém, as pessoas já não dependem tanto da serialização personalizada do Java e preferem usar JSON. JSON é o formato de serialização de dados mais difundido: é legível por humanos e não é específico do Java.

Uma das bibliotecas mais usadas é a jackson-databind, que oferece um ObjectMapper para transformar objetos em JSON e vice-versa.

Por ser uma biblioteca popular, é muito importante saber que a biblioteca jackson-databind foi alvo de muitas vulnerabilidades divulgadas nos últimos anos. Ainda assim, isso não significa que usar o Jackson ObjectMapper represente, por padrão, um risco à segurança. Neste artigo, vamos explicar como funcionam as vulnerabilidades de desserialização do Jackson e como verificar se você está protegido.

Mas, antes de irmos longe demais, vamos responder a algumas perguntas que você talvez tenha...

O que é desserialização?

Desserialização é o processo de transformar dados de um arquivo ou fluxo novamente em um objeto para uso na sua aplicação. Esses dados podem ser binários ou estruturados, como JSON e XML. A desserialização é o oposto da serialização, que transforma objetos em fluxos de bytes ou texto estruturado.

Para que serve o Jackson ObjectMapper?

O Jackson ObjectMapper faz parte da biblioteca Jackson databind e é usado para transformar JSON em objetos Java e vice-versa. É uma das bibliotecas mais conhecidas e usadas no ecossistema Java para converter dados entre JSON e Java, e já vem incluído no Spring Boot.

O Jackson ObjectMapper é seguro?

Sim, o Jackson ObjectMapper é seguro e usa configurações seguras por padrão. É essencial atualizá-lo para as versões mais recentes para evitar problemas de segurança conhecidos.

Criar um ObjectMapper é caro?

Criar um ObjectMapper tem um custo considerável. Por isso, recomenda-se reutilizar a mesma instância. O Jackson ObjectMapper é seguro para uso por várias threads, então pode ser reutilizado sem riscos.

Usando o Jackson ObjectMapper para criar objetos Java a partir de JSON

Com o código abaixo, podemos criar um ObjectMapper e usá-lo para recriar uma Person a partir de uma string JSON obtida de um arquivo.

ObjectMapper om = new ObjectMapper();
Person myvalue = om.readValue(Files.readAllBytes(Paths.get("person.json")), Person.class);
System.out.println("name:"+myvalue.name+" \n"+"Age:"+myvalue.age);

Tudo é bem simples, sem nada de especial. Sem anotações, o Jackson ObjectMapper usa reflexão para fazer o mapeamento para POJOs. Graças à reflexão, ele funciona com todos os campos, independentemente do modificador de acesso. Se houver getters e setters disponíveis, o Jackson ObjectMapper os usará para fazer o mapeamento.

Tipagem padrão no Jackson

Muitas das vulnerabilidades da biblioteca Jackson para serialização JSON dependem da tipagem padrão, que não vem ativada por padrão. É necessário ativá-la explicitamente. Isso significa que, na maioria dos casos, as vulnerabilidades desta lista não afetam diretamente o seu sistema.

Mas vamos explicar o que é a tipagem padrão e para que ela serve.

A tipagem padrão é um mecanismo do Jackson ObjectMapper para lidar com tipos polimórficos e herança. Se você quiser desserializar um JSON para um POJO Java, mas não souber qual é o subtipo do objeto ou do campo, basta desserializá-lo para a superclasse.

Imagine que você tenha Coffee e Tea. As duas classes têm a mesma superclasse: HotDrink. Portanto, se o seu Breakfast contiver uma HotDrink, mas você não souber se ela é Coffee ou Tea, poderá usar a tipagem padrão para resolver isso.

public class Breakfast {
   public String food;
   public HotDrink drink;
}

public abstract class HotDrink {
   public String name;
}

public class Coffee extends HotDrink {
   @Override
   public String toString() {
       return String.format("Coffee{name='%s'}", name);
   }
}

public class Tea extends HotDrink {
   @Override
   public String toString() {
       return String.format("Tea{name='%s'}", name);
   }
}

String breakfastJson = """
       {
           "food":"sandwich",
           "drink":["nl.brianvermeer.example.jackson.serialization.Tea",{"name":"oolong"}]
       }
       """;

var om = new ObjectMapper();
om.enableDefaultTyping();

var myBreakfast = om.readValue(breakfastJson, Breakfast.class);
System.out.println("breakfast hotdrink:"+myBreakfast.drink);

Neste exemplo, ativei a tipagem padrão no ObjectMapper para lidar com polimorfismo em todos os lugares em que uso esse ObjectMapper. Você também pode fazer isso em um campo específico usando a anotação @JsonTypeInfo.

Problemas de segurança com a tipagem padrão no Jackson ObjectMapper

Com a tipagem padrão ativada globalmente, é possível levar a herança ao extremo. Se o seu Breakfast não contiver uma HotDrink, mas um campo do tipo Object, qualquer objeto disponível no classpath poderá ser usado. Isso também significa que podemos desserializar qualquer objeto disponível no classpath. Potencialmente, esse objeto pode ser um gadget que cria uma cadeia de gadgets e, por fim, leva à execução remota.

Essas cadeias de gadgets são bem parecidas com as descritas no meu artigo sobre serialização e desserialização em Java. Vamos simplificar com um único gadget que executa um comando assim que é inicializado. Veja abaixo minha classe SecondBreakfast, que contém uma bebida do tipo Object.

public class Gadget {

   private Runnable command;

   public Gadget(String value) {
       this.command = new Command(value);
       this.command.run();
   }
}

public class SecondBreakfast {
   public String food;
   public Object drink;
}

Se eu desserializar meu SecondBreakfast com a tipagem padrão ativada, posso desserializar um Object que permite a execução arbitrária de código.

String secondBreakfastJson = """
       {
           "food":"sandwich",
           "drink":["nl.brianvermeer.example.jackson.serialization.Gadget", "rm -rf *"]
       }
       """;

var om = new ObjectMapper();
om.enableDefaultTyping();

Var mySecondBreakfast = om.readValue(secondBreakfastJson, SecondBreakfast.class);
System.out.println("Second breakfast hotdrink:"+mySecondBreakfast.drink);

Este é um exemplo simplificado. Encontrar e criar uma cadeia de gadgets não é fácil, mas certamente é possível. Com todas as bibliotecas e frameworks que usamos, é provável que uma combinação de classes do seu classpath possa ser usada para criar uma cadeia desse tipo.

Além disso, já existe um grande conjunto de “classes maliciosas” conhecidas. A desserialização de uma dessas classes é considerada perigosa e, basicamente, segue o padrão descrito acima. Para consultar exemplos, veja a lista de vulnerabilidades de desserialização da biblioteca jackson-databind na Snyk Vulnerability Database.

Como isso afeta minha aplicação?

Tudo bem, não entre em pânico: não é tão grave. Primeiro, os mantenedores da biblioteca jackson-databind bloqueiam ativamente o conjunto de “classes maliciosas” no SubTypeValidator. Segundo, é necessário ativar os tipos padrão explicitamente. Isso significa que, por padrão, essa configuração fica desativada e a desserialização polimórfica nem sequer é possível.

A análise com ferramentas de SCA, como o Snyk, identifica a vulnerabilidade no resultado. Uma busca rápida mostra que muitos desses problemas ocorreram no passado. É sempre recomendável atualizar para a versão mais recente, pois a biblioteca recebe manutenção ativa e novas “classes maliciosas” são bloqueadas assim que são encontradas. Ainda assim, a melhor opção é evitar ativar a tipagem padrão no seu ObjectMapper do Jackson.

O assistente de triagem da Snyk entra em ação

A Snyk está trabalhando para filtrar essas vulnerabilidades quando o seu código não atende aos pré-requisitos. No caso do Jackson ObjectMapper, se o seu código não ativar a tipagem polimórfica, vamos indicar que é improvável que essa vulnerabilidade específica afete você. No momento da publicação deste artigo, esse recurso está disponível apenas para a vulnerabilidade de desserialização do Jackson, mas a equipe continua trabalhando para aprimorá-lo e ampliá-lo. Para usar esse recurso, precisamos da sua autorização para analisar o código e verificar se a tipagem padrão está desativada.

Detalhes da vulnerabilidade do Snyk na desserialização de dados não confiáveis com Jackson Databind, mostrando a CVE-2019-12384, gravidade alta e versões corrigidas.

Os exemplos de código deste artigo estão publicados na minha conta do GitHub. Fique à vontade para criar um fork ou reutilizá-los como preferir.

Desserialize com segurança!

A melhor maneira de evitar problemas de desserialização com o Jackson ObjectMapper é impedir a tipagem polimórfica. Não ative a tipagem padrão no seu ObjectMapper. Além disso, conecte seu projeto à Snyk para verificar se você está usando uma biblioteca jackson-databind com vulnerabilidades conhecidas. Na maioria dos casos, é fácil substituí-la por uma versão mais recente. Ao ativar a análise de código, o Triage Assistant pode ajudar você a determinar se a vulnerabilidade provavelmente pode ou não ser explorada.

Comece a resolver desafios de capture the flag

Aprenda a resolver desafios de capture the flag assistindo sob demanda ao nosso workshop virtual introdutório.