Skip to main content

Nuevas funciones de Java 17 para mejorar la seguridad y la serialización

Escrito por
Blog Feature Java deserialize

21 de octubre de 2021

0 minutos de lectura

En diciembre de 2020, escribí el artículo Serialización y deserialización en Java: explicación de la vulnerabilidad de deserialización de Java sobre los problemas de la implementación de serialización personalizada de Java. El marco de serialización está tan profundamente integrado en Java que es importante saber lo peligrosas que pueden ser algunas implementaciones. La deserialización insegura puede dar lugar a la ejecución de código arbitrario si se crea una cadena de gadgets a partir de las clases de tu classpath.

Hace poco se lanzó Java 17, la nueva versión LTS. Pero ¿cómo afectan las nuevas funciones a este problema y pueden ayudarnos a prevenir mejor las vulnerabilidades de deserialización?

En esta publicación del blog, analizaré tres funciones principales de Java 17:

  1. Records

  2. Mejoras de Java Flight Recorder (JFR)

  3. JEP 415 (propuesta de mejora de Java): filtros de deserialización específicos del contexto.

1. Records

Los records se introdujeron en Java 14 como una función preliminar y se lanzaron oficialmente en Java 16. Sin embargo, como muchos desarrolladores prefieren actualizar solo a versiones LTS, tiene sentido hablar de los records en el contexto de la serialización ahora que Java 17 ya está disponible.

A diferencia de lo que ocurre con los POJO normales, al deserializar un record se usa el constructor para volver a crear el objeto. Esto no sucede con los objetos Java comunes, y el marco depende en gran medida de la reflexión. Esto significa que la lógica que se activa desde el constructor no se ejecuta al deserializar un objeto Java común. Lee mi publicación anterior del blog para obtener más información. En el caso de los records, no hay magia al volver a crear el objeto durante la deserialización. Si por algún motivo incluyes lógica de validación en el constructor de un record, ahora sabemos que esta lógica se aplicará,

Aun así, podemos debatir si deberías incluir lógica en un record. Si lo haces de forma incorrecta, puedes crear gadgets que formen parte de una cadena de gadgets de deserialización. Y lo que es más importante, seguimos usando la función readObject() para deserializar. Esto significa que seguimos siendo vulnerables a las cadenas de gadgets en los POJO normales, independientemente de los records.

2. Filtros de deserialización en Java

Para abordar las vulnerabilidades de deserialización en Java, es posible configurar filtros de serialización. Esta función se introdujo en Java 9 con la implementación de JEP 290. Puedes establecer límites para el tamaño de los arreglos, la profundidad del grafo, el total de referencias y el tamaño del flujo. Además, puedes crear listas de bloqueo y de permitidos basadas en patrones para limitar las clases que quieres deserializar.

Puedes configurar un filtro de este tipo como filtro global de la JVM o de forma individual para cada flujo. Para el filtro global, puedes establecer un argumento de la JVM o configurarlo en el código. A continuación, creé un filtro que permite todas las clases de mypackage y bloquea todo lo demás.

Argumento de la JVM:

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

Código:

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

También puedes configurar un filtro para un flujo específico, como se muestra a continuación.

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

Hasta Java 17, al configurar un filtro para un flujo específico, se reemplaza el filtro global en ese flujo. No se combinan de ninguna manera el filtro global y el filtro específico del flujo. Esta forma de trabajar no es muy flexible. Además, plantea el problema de que el filtro global podría no funcionar si una biblioteca que incluyes se encarga de la deserialización.

Filtros de deserialización específicos del contexto en Java 17

Java 17 mejoró el filtro de deserialización con la implementación de JEP 415.

Una de las cosas más importantes que puedes hacer ahora es configurar un SerialFilterFactory en ObjectInputFilter.Config. Esta fábrica debe ser un BinaryOperator y describe qué hacer cuando se agrega un filtro específico a un flujo determinado.

En el siguiente ejemplo, configuro una fábrica muy básica en Config que usa el método de combinación predeterminado para combinar el filtro existente con el nuevo. Con esta herramienta puedo decidir si los filtros deben combinarse y cómo hacerlo. Esto resuelve el problema que mencioné antes sobre cómo las bibliotecas manejan el filtro global.

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

Además de Filter Factory, Java 17 también ofrece métodos prácticos para crear filtros fácilmente. En mi opinión, funciones como allowFilter() y rejectFilter() en ObjectInputFilter permiten crear filtros de forma más declarativa y legible.

En el siguiente ejemplo de código de Java 17, uso estas nuevas funciones. En el método de deserialización del ejemplo, rechazo específicamente la clase Gadget. Tanto la clase Gadget como el record TwoValue pertenecen al mismo paquete. El filtro rechazará Gadget y permitirá todas las demás clases de este paquete.

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 deserialización de Java Flight Recorder

El lanzamiento de Java 17 también incluye una excelente incorporación a Java Flight Recorder (JFR) para ayudarte en tu lucha contra los exploits de deserialización. Esta nueva versión de Java admite un evento específico para monitorear la deserialización. Se crea un evento de deserialización para cada objeto de un flujo y se registran datos interesantes, como el tipo real, si había un filtro, si se filtró el objeto, la profundidad del objeto, el número de referencias, etc.

Toda esta información es útil para detectar si hay deserialización en alguna parte de tu aplicación y qué se está deserializando mientras se ejecuta el proceso. Sin embargo, debes asegurarte de habilitar este evento. No se captura de forma predeterminada, por lo que debes crear una configuración específica, como esta.

<?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>

Tomemos el ejemplo de código anterior, pero ahora deserialicemos la clase Gadget. Obtengo el siguiente resultado al ejecutar el código en IntelliJ IDEA con Java Flight Recorder y la configuración personalizada.

Vista de Events del generador de perfiles de Java que muestra un evento de deserialización rechazado y sus propiedades, incluidos el tipo, los bytes leídos y el estado del filtro.

Puedes ver que el evento captura el tipo real de objeto durante la deserialización y que el filtro lo rechaza. Si quieres saber más sobre cómo usar este evento de deserialización específico de JFR, consulta la publicación del blog Monitorear la deserialización para mejorar la seguridad de las aplicaciones, de Chris Hegarty.

Actualiza a Java 17 para tener herramientas más potentes contra los exploits de deserialización insegura

La versión LTS de Java 17 incluye mejoras importantes para prevenir la deserialización maliciosa en tus aplicaciones Java. Por eso, actualizar a una versión más reciente, como la 17, es fundamental si quieres adoptar estas prácticas. Aun así, en mi opinión, es mejor evitar por completo la serialización personalizada de Java. Sin embargo, si tienes que usarla o dependes de una biblioteca que la utiliza, ya sabes cómo protegerte.

Además, procura no importar bibliotecas que incluyan cadenas de gadgets de deserialización conocidas o que tengan otros problemas de seguridad relacionados con la deserialización.

Empieza con los retos de Capture the Flag

Aprende a resolver retos de Capture the Flag viendo nuestro taller virtual de nivel básico 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

Por qué los agentes de programación con IA siguen generando fallas de control de acceso

Los agentes de programación con IA pueden generar lógica de autorización que compila y supera la revisión, pero expone los datos de un inquilino a otro. Descubre por qué es difícil detectar el control de acceso roto y cómo prevenirlo.