Skip to main content

Problemas de deserialización JSON de Java con Jackson ObjectMapper

Escrito por
Blog Feature Java deserialize

1 de diciembre de 2021

0 minutos de lectura

En una publicación anterior del blog, analizamos la plataforma de serialización personalizada de Java y sus implicaciones de seguridad. Más recientemente, escribí sobre cómo las mejoras de Java 17 pueden ayudarte a prevenir la deserialización insegura. Sin embargo, hoy en día, las personas ya no dependen tanto de la serialización personalizada de Java, sino que optan por usar JSON. JSON es el formato de serialización de datos más difundido, es legible para las personas y no es específico de Java.

Una de las bibliotecas más utilizadas es jackson-databind, que te ofrece un ObjectMapper para transformar tus objetos en JSON y viceversa.

Como es una biblioteca popular, es muy importante saber que jackson-databind ha tenido muchas vulnerabilidades reportadas en los últimos años. Sin embargo, esto no significa que usar Jackson ObjectMapper sea un riesgo de seguridad por defecto. En este artículo explicaremos cómo funcionan las vulnerabilidades de deserialización de Jackson y cómo asegurarte de que no te afecten.

Pero antes de profundizar, respondamos algunas preguntas que quizá tengas...

¿Qué es la deserialización?

La deserialización consiste en transformar los datos de un archivo o flujo de datos nuevamente en un objeto para usarlo en tu aplicación. Estos datos pueden ser binarios o estar estructurados en formatos como JSON y XML. La deserialización es lo opuesto a la serialización, que transforma objetos en flujos de bytes o texto estructurado.

¿Para qué se usa Jackson ObjectMapper?

Jackson ObjectMapper forma parte de la biblioteca Jackson databind y se usa para transformar JSON en objetos Java y viceversa. Es una de las bibliotecas más utilizadas y conocidas del ecosistema Java para convertir JSON a Java y viceversa, y viene incluida automáticamente con Spring Boot.

¿Jackson ObjectMapper es seguro?

Sí, Jackson ObjectMapper es seguro y utiliza valores predeterminados seguros. Es fundamental actualizarlo a las versiones más recientes para evitar problemas de seguridad conocidos.

¿Es costoso crear un ObjectMapper?

Crear un ObjectMapper es bastante costoso. Por eso, se recomienda reutilizar la instancia de ObjectMapper. Jackson ObjectMapper es seguro para subprocesos, así que puedes reutilizarlo sin problemas.

Usar Jackson ObjectMapper para crear objetos Java a partir de JSON

Con el código siguiente, podemos crear un ObjectMapper y usarlo para volver a crear un Person a partir de una cadena JSON proveniente de un archivo.

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);

Esto es muy sencillo y no sucederá nada complicado. Sin anotaciones, Jackson ObjectMapper usa la reflexión para realizar la asignación a POJO. Gracias a la reflexión, funciona con todos los campos, sin importar el modificador de acceso. Si hay getters y setters disponibles, Jackson ObjectMapper los usará para realizar la asignación.

Tipado predeterminado en Jackson

Muchas de las vulnerabilidades de la biblioteca Jackson para la serialización JSON dependen del tipado predeterminado, que no está habilitado de forma predeterminada. Debes habilitarlo explícitamente. Esto significa que, en la mayoría de los casos, las vulnerabilidades mencionadas en esta lista no afectan directamente a tu sistema.

Pero expliquemos qué es el tipado predeterminado y para qué se usa.

El tipado predeterminado es un mecanismo de Jackson ObjectMapper para manejar tipos polimórficos y herencia. Si quieres deserializar JSON a un POJO de Java, pero no sabes cuál es el subtipo del objeto o del campo, puedes deserializarlo simplemente a la superclase.

Supongamos que tienes Coffee y Tea. Ambas clases tienen la misma superclase: HotDrink. Así que, si tu Breakfast contiene un HotDrink, pero no sabes si es Coffee o Tea, puedes usar el tipado predeterminado para resolverlo.

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);

En este ejemplo, habilité el tipado predeterminado en ObjectMapper para poder manejar el polimorfismo en todos los lugares donde uso este ObjectMapper. También puedes hacerlo en un campo específico mediante la anotación @JsonTypeInfo.

Problemas de seguridad con el tipado predeterminado en Jackson ObjectMapper

Si el tipado predeterminado está habilitado de forma global, es posible llevar la herencia al extremo. Si tu Breakfast no contiene un HotDrink, sino un campo de tipo Object, entonces puede usarse cualquier objeto disponible en el classpath. Esto también significa que podemos deserializar cualquier objeto disponible en el classpath. Potencialmente, podría tratarse de un objeto gadget que configure una cadena de gadgets y que, finalmente, permita la ejecución remota.

Estas cadenas de gadgets son bastante similares a las que describí en mi publicación del blog Serialización y deserialización en Java. Simplifiquemos esto con un solo gadget que ejecuta un comando justo cuando se inicializa. La clase SecondBreakfast de abajo contiene una bebida de 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;
}

Si deserializo mi SecondBreakfast con el tipado predeterminado habilitado, puedo deserializar un Object que contiene código arbitrario para ejecutar.

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 es un ejemplo simplificado. Aunque encontrar y crear una cadena de gadgets no es fácil, definitivamente es posible. Debido a todas las bibliotecas y los frameworks que usamos, es probable que una combinación de todas las clases de tu classpath sirva para crear una cadena de gadgets de este tipo.

Además, ya se ha identificado un gran conjunto de “clases peligrosas” conocidas. La deserialización de una clase de este tipo se considera peligrosa y, básicamente, sigue el patrón descrito anteriormente. Consulta la lista de vulnerabilidades de deserialización de la biblioteca jackson-databind en Snyk Vulnerability Database.

¿Cómo afecta esto a mi aplicación?

Bien, no entres en pánico: no es tan grave. En primer lugar, quienes mantienen la biblioteca jackson-databind bloquean activamente el conjunto de “clases peligrosas” en SubTypeValidator. En segundo lugar, debes habilitar explícitamente los tipos predeterminados. Esto significa que, de forma predeterminada, esta opción está desactivada y ni siquiera es posible realizar la deserialización polimórfica.

El análisis con herramientas de SCA como Snyk muestra la vulnerabilidad en los resultados. Una búsqueda rápida muestra que en el pasado hubo muchos problemas de este tipo. Siempre es recomendable actualizar a la versión más reciente porque la biblioteca recibe mantenimiento constante y las nuevas “clases peligrosas” se bloquean activamente cuando se detectan. Aun así, lo mejor es evitar habilitar el tipado predeterminado de tu ObjectMapper de Jackson.

El asistente de triaje de Snyk al rescate

Snyk está trabajando para filtrar estas vulnerabilidades cuando tu código no cumple los requisitos previos. En el caso de Jackson ObjectMapper, esto significa que, si tu código no habilita el tipado polimórfico, te indicaremos que es poco probable que esta vulnerabilidad específica pueda explotarse en tu sistema. Al momento de escribir esto, esta función solo está disponible para la vulnerabilidad de deserialización de Jackson, pero el equipo sigue trabajando para mejorarla y ampliarla. Para usar esta función, necesitamos tu aprobación para analizar tu código y comprobar que el tipado predeterminado no esté habilitado.

Detalles de la vulnerabilidad de Snyk para la deserialización de datos no confiables en Jackson databind: CVE-2019-12384, gravedad alta y versiones corregidas.

Los ejemplos de código de esta publicación del blog están disponibles en mi cuenta de GitHub. Siéntete en libertad de bifurcarlos o reutilizarlos como prefieras.

¡Deserializa de forma segura!

La mejor forma de evitar problemas de deserialización con Jackson ObjectMapper es impedir el tipado polimórfico. No habilites el tipado predeterminado en tu ObjectMapper. Además, conecta tu proyecto a Snyk para descubrir si usas una biblioteca jackson-databind con vulnerabilidades conocidas. En la mayoría de los casos, puedes reemplazarla fácilmente por una versión más reciente. Cuando habilitas el análisis de código, Triage Assistant puede ayudarte a determinar si es probable o improbable que se pueda explotar.

Empieza con los desafíos de Capture the Flag

Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.

Leer más

Blog

Los modelos de frontera encontraron las vulnerabilidades. Solo el atacante encontró las cadenas.

El análisis estático encontró las fallas, pero solo las pruebas de ataque en vivo demostraron cómo podían encadenarse para provocar brechas. Una comparación de Evo COS, Claude Security y Claude Code Security.

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

Blog

Tu backlog de vulnerabilidades ya no es deuda técnica: es una superficie de ataque

Un backlog de vulnerabilidades en crecimiento es más que deuda técnica: es una superficie de ataque. Descubre por qué las suposiciones obsoletas sobre el riesgo, los atacantes automatizados y los hallazgos encadenados requieren un nuevo enfoque.