Nuevas funciones de Java 17 para mejorar la seguridad y la serialización
21 de octubre de 2021
0 minutos de lecturaEn 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:
Records
Mejoras de Java Flight Recorder (JFR)
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:
Código:
También puedes configurar un filtro para un flujo específico, como se muestra a continuación.
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.
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.
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.
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.

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.



