1. Buenas prácticas principales para una serialización segura
La serialización es como empaquetar el equipaje en el aeropuerto: si no sabes qué hay dentro y a quién le confías la maleta, puedes llevarte una desagradable sorpresa en el control de seguridad. En Java, la serialización permite guardar y restaurar objetos fácilmente, pero abre la puerta a todo un espectro de ataques si los datos provienen de fuentes no fiables.
Amenaza clásica:
La serialización en Java puede ser insegura. Si un atacante introduce un flujo malicioso, durante la deserialización pueden producirse consecuencias muy desagradables: desde la modificación de campos hasta la ejecución de código no deseado. No es un cuento de miedo de un libro de texto: en la historia de Java realmente ha habido casos en los que los ataques se basaban precisamente en este mecanismo.
¿Por qué ocurre esto?
El caso es que la deserialización no es solo restaurar valores de campos. En el proceso se crea un objeto completo: pueden invocarse métodos especiales (por ejemplo, readObject, readResolve) y, a veces, activarse puntos vulnerables en el código mediante reflexión. Son especialmente peligrosas las clases de bibliotecas de terceros: algunas ejecutan acciones ya en la etapa de deserialización. Por eso, nunca confíes en datos serializados obtenidos del exterior.
Usa transient para datos sensibles
Si tu clase tiene campos que contienen contraseñas, tokens, claves privadas u otra información sensible, decláralos como transient. Estos datos no se incluirán en el flujo serializado.
import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // no se serializa
// ...constructores, getters, setters...
}
¿Qué ocurrirá durante la deserialización? El campo password tendrá el valor por defecto (null para cadenas). Esto es bueno: las contraseñas no se guardarán en archivos ni se enviarán por la red.
Define explícitamente serialVersionUID
Indica siempre serialVersionUID explícitamente. Esto reduce la probabilidad de errores de compatibilidad y minimiza el riesgo de suplantación de clases durante la deserialización.
private static final long serialVersionUID = 1L;
¿Por qué es importante para la seguridad? Si no se indica serialVersionUID, el compilador lo generará automáticamente en función de la estructura de la clase. Esto puede provocar discrepancias inesperadas y, en teoría, abusos mediante la sustitución de clases con el mismo nombre pero otra estructura.
Comprueba los tipos de los objetos al deserializar
No confíes en lo que llega por la red o desde un archivo. Tras la deserialización, verifica siempre que el objeto obtenido sea del tipo esperado antes de trabajar con él.
Object obj = objectInputStream.readObject();
if (obj instanceof User) {
User user = (User) obj;
// trabajamos de forma segura con user
} else {
// tipo inesperado — lanzar una excepción o manejar el error
}
¿Para qué sirve? Un flujo malicioso puede contener un objeto de otra clase que implemente Serializable, pero que no se ajuste a tu lógica de negocio.
Restringe las clases que pueden deserializarse (ObjectInputFilter)
A partir de Java 9, utiliza filtros — ObjectInputFilter — para limitar el conjunto de clases que se permite deserializar. Es como un control de acceso en la entrada.
Ejemplo: configuración del filtro
import java.io.*;
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;com.example.Address;!*"
);
ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);
Object obj = in.readObject(); // ahora solo se deserializan User y Address
Este filtro permite únicamente las clases User y Address de tu aplicación. Todas las demás se bloquearán — se lanzará una excepción. Esto reduce significativamente el riesgo de que entre un objeto malicioso.
No deserialices datos de orígenes no fiables
Regla de oro: si no estás seguro del origen de los datos, no deserialices. Prefiere formatos que no ejecuten código durante el parseo (por ejemplo, JSON, XML con parsers seguros).
Ejemplo de mala práctica:
// ¡Nunca hagas esto con datos de Internet!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // ¡peligroso!
¿Qué es mejor hacer?
- Usar parsers JSON (por ejemplo, Gson/Jackson) o parsers XML con validación.
- Si la serialización binaria es necesaria, filtra las clases mediante ObjectInputFilter y verifica los tipos (instanceof).
Usa formatos alternativos para el intercambio con sistemas externos
Para integraciones, utiliza formatos que no ejecuten código durante el parseo: JSON, XML, Protocol Buffers, etc. Esto casi elimina los ataques mediante deserialización.
// En lugar de ObjectInputStream usa un parser JSON
User user = gson.fromJson(jsonString, User.class);
No almacenes objetos serializados en lugares de acceso público
Los archivos con objetos serializados pueden contener datos sensibles. No los almacenes en directorios públicos y limita los permisos a nivel del sistema de archivos.
No confíes en la serialización para el control de integridad
La serialización no garantiza la integridad ni la autenticidad de los datos. Utiliza firmas digitales, sumas de verificación o cifrado si los cambios no son aceptables.
2. Práctica: ejemplo de ObjectInputFilter y demostración de una vulnerabilidad
Ejemplo de filtrado de clases
Supongamos que tenemos una clase User:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password;
// ...constructores, getters, setters...
}
El filtro permite solo User:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;!*"
);
in.setObjectInputFilter(filter);
Ahora, si alguien intenta colar un objeto de otra clase, la deserialización terminará con un error.
Demostración de una posible vulnerabilidad
Clase maliciosa:
// Imaginemos que alguien coló esta clase
public class Evil implements java.io.Serializable {
static {
System.out.println("¡Se ejecutó código malicioso!");
// aquí podría haber cualquier cosa...
}
}
Si no filtras las clases, durante la deserialización se puede crear un objeto Evil, y el inicializador estático se ejecutará al cargar la clase — esto ya es un ataque real.
4. Errores típicos al garantizar la seguridad de la serialización
Error n.º 1: deserialización sin filtrado ni verificación del tipo. A menudo los desarrolladores leen un objeto del flujo y lo convierten directamente al tipo necesario. Esto abre la puerta a ataques. Usa ObjectInputFilter y comprueba el tipo con instanceof.
Error n.º 2: almacenamiento de datos sensibles sin transient. Si olvidas declarar contraseñas/llaves como transient, acabarán en el flujo y pueden filtrarse junto con el archivo.
Error n.º 3: ausencia de serialVersionUID. Sin un serialVersionUID explícito, son posibles errores de compatibilidad inesperados y riesgos asociados a la sustitución de clases.
Error n.º 4: usar la serialización para el intercambio con sistemas externos. La serialización binaria es conveniente dentro de la aplicación (por ejemplo, caché), pero peligrosa para el intercambio externo. Prefiere JSON/XML/Proto con parsers seguros.
Error n.º 5: ignorar la integridad de los datos. La modificación de los bytes de un archivo serializado pasará desapercibida. Aplica firmas digitales, sumas de verificación o cifrado.
GO TO FULL VERSION