1. Seguridad de la serialización binaria
La serialización en Java no es solo guardar los campos de un objeto. Es la capacidad de «reconstruir» cualquier objeto con cualquier contenido, siempre que implemente la interfaz Serializable. ¡Parece cómodo! Pero si tu aplicación deserializa datos de una fuente no confiable (por ejemplo, de la red o de un archivo que un atacante pudo haber sustituido), corre el riesgo de convertirse en víctima de ataques.
¿Cómo funciona?
Durante la deserialización, Java crea objetos a partir de un flujo de bytes sin invocar los constructores de las clases. Si en la clase se implementan métodos especiales como readObject, se llamarán automáticamente. Un atacante puede «construir» un flujo de bytes de modo que, al deserializar, se ejecute código vulnerable.
Ejemplo: ataque de «gadget chain»
Imagina que tienes una clase que, al deserializar, lanza un comando externo (lee un archivo o invoca el shell). Si el atacante sabe que la aplicación deserializa objetos de ciertos tipos, puede inyectar un flujo de bytes especialmente preparado que conduzca a la ejecución de código malicioso. Estos ataques se construyen como una «cadena de gadgets»: una secuencia de invocaciones que termina en una operación peligrosa.
¿Por qué es tan crítico?
La deserialización es un proceso en el que, a partir de un flujo de datos, se reconstruyen objetos reales y, en ese momento, puede ejecutarse código arbitrario. Si los datos provienen de una fuente no confiable, esto abre la puerta a la ejecución remota de comandos (RCE). Grandes compañías han publicado recomendaciones para evitar la deserialización insegura; en proyectos corporativos modernos, la serialización binaria a menudo está prohibida por políticas de seguridad.
¿Cómo protegerse?
- No deserialices nunca objetos de fuentes no confiables.
- Utiliza lista blanca (whitelisting): una lista explícita de tipos permitidos para la deserialización.
- Prefiere formatos de texto para integraciones externas: JSON, XML.
- Si la serialización es inevitable, usa bibliotecas con opciones de deserialización segura (por ejemplo, Jackson con restricción de tipos).
- Limita el uso de métodos no estándar de serialización (readObject, readResolve, etc.) si no estás seguro de su seguridad.
2. Compatibilidad entre versiones de clases
La serialización binaria en Java está estrechamente ligada a la estructura de la clase. Serializaste un objeto en la versión 1.0, luego actualizaste la clase (añadiste/eliminaste un campo): intentar deserializar el objeto «antiguo» en la nueva versión puede provocar un error o pérdida de datos.
¿Cómo determina Java la compatibilidad?
Para ello se utiliza un campo especial: serialVersionUID. Es el identificador de versión de la clase. Si el objeto serializado tiene un serialVersionUID y la clase actual otro distinto, se lanzará InvalidClassException y la deserialización no se llevará a cabo.
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // versión especificada explícitamente
private String name;
private int age;
}
Si cambias la estructura de la clase (por ejemplo, añades el campo email) y no cambias serialVersionUID, Java considerará que la clase es compatible e intentará deserializar el objeto antiguo. Si no has indicado serialVersionUID explícitamente, la JVM lo generará automáticamente en función de la estructura, y cualquier cambio provocará incompatibilidad.
¿Qué ocurre cuando no coinciden/coinciden las versiones?
Si los identificadores no coinciden, la deserialización no se realizará: InvalidClassException. Si coinciden, los campos se emparejan por nombre y tipo: los campos nuevos recibirán valores por defecto (null, 0) y los eliminados se ignorarán. Al cambiar el tipo o el nombre de un campo, son posibles errores e interpretación incorrecta de los datos.
Consejo práctico. En las clases serializables, define siempre un serialVersionUID explícito. Cámbialo solo cuando haya cambios incompatibles (eliminación/cambio de tipo de un campo importante). Al añadir campos nuevos, el identificador puede permanecer igual: la JVM procesará correctamente los objetos antiguos.
Tabla: Qué ocurre al cambiar la clase
| Cambio en la clase | ¿Qué ocurrirá al deserializar? |
|---|---|
| Se añadió un nuevo campo | Recibirá el valor por defecto (0, null) |
| Se eliminó un campo | Se ignora al leer datos antiguos |
| Se cambió el tipo de un campo | Excepción o datos incorrectos |
| Se cambió el nombre de un campo | El campo antiguo se ignora; el nuevo toma el valor por defecto |
| Se cambió serialVersionUID | Excepción InvalidClassException |
3. Limitaciones de la serialización estándar
No todos los objetos se pueden serializar
Los campos con los modificadores transient y static no se serializan. static porque pertenece a la clase, no al objeto; transient porque has prohibido explícitamente la serialización del campo.
Algunos objetos por definición no son serializables: Thread, conexiones a bases de datos, sockets, Scanner, etc. Si tu clase tiene un campo de ese tipo y no es transient, obtendrás NotSerializableException.
import java.io.Serializable;
import java.util.Scanner;
public class Session implements Serializable {
private transient Scanner scanner; // ¡no se serializa!
private String login;
}
Problemas de rendimiento y extensibilidad
La serialización de grafos de objetos grandes puede ser lenta y con alto consumo de memoria.
El formato binario se adapta mal a integraciones con otras plataformas e idiomas: solo lo «entiende» Java.
Es difícil controlar qué se serializa exactamente, especialmente con jerarquías profundas y referencias cíclicas.
Problemas al mantener datos antiguos
El almacenamiento a largo plazo de instantáneas binarias es un riesgo. Al cabo de uno o dos años, la estructura de las clases cambia y los archivos antiguos dejan de cargarse.
Historia real: «Guardamos una caché de usuarios serializada hace tres años, actualizamos la aplicación y ahora no podemos cargarla. ¡Hola, datos perdidos!»
4. Buenas prácticas: cómo no caer en las trampas
- Usa la serialización binaria solo para tareas internas donde controles ambos extremos del proceso.
- No uses la serialización binaria para integraciones externas ni para almacenamiento a largo plazo de datos importantes.
- Indica siempre explícitamente serialVersionUID en las clases serializables.
- Marca con el modificador transient los campos que no deban serializarse.
- Para intercambiar con sistemas externos, usa formatos de texto y bibliotecas modernas: JSON, XML, Jackson, Gson, JAXB.
- Para la compatibilidad, usa versionado: almacena la versión del objeto en la propia clase y adapta el procesamiento durante la deserialización.
- Si la serialización es solo para caché, no mantengas la compatibilidad a toda costa: es más fácil recalcular el caché.
- No almacenes datos sensibles (contraseñas, claves) en objetos serializables: la serialización no cifra los datos.
5. Errores típicos al trabajar con serialización binaria
Error n.º 1: deserializar datos de una fuente no confiable. El error más peligroso es aceptar y deserializar objetos que vienen «de la calle» (de la red, del usuario, de un archivo manipulado). Es un camino directo a vulnerabilidades hasta llegar a RCE.
Error n.º 2: cambiar implícitamente la estructura de la clase sin actualizar serialVersionUID. Si no indicas el identificador explícitamente, la JVM lo generará automáticamente. Cualquier cambio en la estructura (incluso el orden de los campos) llevará a incompatibilidad e imposibilidad de cargar objetos antiguos.
Error n.º 3: intentar serializar objetos con campos no serializables. Si una clase tiene un campo que no implementa Serializable y no es transient, la serialización terminará con una excepción.
Error n.º 4: guardar en objetos serializables datos temporales o sensibles. Tokens, contraseñas, descriptores temporales de recursos: todo ello puede acabar accidentalmente en un archivo.
Error n.º 5: usar la serialización binaria para almacenamiento a largo plazo e intercambio entre versiones. Tras la primera actualización de las clases, el riesgo de datos corruptos y problemas de compatibilidad es alto.
Error n.º 6: esperar que los campos static y transient «se recuperen» después de la deserialización. Estos campos no se serializan; tras la carga tendrán los valores por defecto.
GO TO FULL VERSION