Guías de seguridad
Deserialización insegura: el riesgo de dejar que los datos externos elijan el tipo de objeto y cómo evitarlo
Algunos formatos dejan que los datos externos elijan qué clase se construye, lo que puede llevar a RCE. Cómo evitarlo: formatos de datos puros, firmas y qué API no usar.
Convertir de nuevo en objetos los bytes recibidos no es peligroso porque el contenido esté mal formado. En algunos formatos, los datos externos deciden qué tipo de objeto se construye.
Por qué la validación de entradas llega demasiado tarde
Formato de datos puro (JSON / XML)
los datos externos deciden solo los valores → los recibes y luego los pasas a tus propios tipos → la validación funciona
Formato que restaura objetos
los datos externos deciden los valores y el tipo → durante la restauración se ejecuta código del tipo elegido → la ejecución nunca llega a tus comprobaciones
La mayor parte de la validación de entradas pregunta si el valor es aceptable. Pero en un formato que restaura objetos, la propia restauración (qué clase se instancia y qué código se ejecuta por el camino) la dirigen los datos externos. Algo puede terminar de ejecutarse antes de que se llame a tu validación.
Es la misma forma que la contaminación de prototipos: los datos externos deciden la estructura en lugar de los valores. Distinto nombre, distinto lenguaje, la misma idea para la defensa.
Qué sale mal (las tres consecuencias que enumera OWASP)
- Denegación de servicio
- Hacer que la propia restauración sea muy costosa, o romperla, para detener la aplicación
- Elusión del control de acceso
- Se construyen a conveniencia del atacante objetos que representan permisos o roles
- Ejecución remota de código
- Encadenar las rutinas que se invocan durante la restauración para llegar a la ejecución de código arbitrario: la consecuencia más grave
«Nuestro framework no hace eso» no es una suposición segura
No es solo una historia de lenguajes antiguos. Cuando contamos las vulnerabilidades de Next.js en los datos del NVD, la deserialización (CWE-502) aparecía en lo alto de la distribución por CWE, porque los frameworks modernos incluyen rutas que deserializan el cuerpo de la petición, entre ellas las Server Actions (¿Next.js y React tienen muchas vulnerabilidades?). «No lo escribí yo» no significa «no está ahí».
Dos defensas
1. No dejes que nada elija tipos (primera opción)
En palabras de OWASP: al pasar a un formato de datos puro como JSON o XML, se reduce la posibilidad de que una lógica de deserialización personalizada se reutilice con fines maliciosos.
Recibe la entrada externa en un formato que solo transporta valores y luego pásala a tus propios tipos en tu propio código. Solo con esto, el atacante ya no puede elegir qué se construye. En la mayoría de los proyectos resuelve el problema.
2. Fírmalo y rechaza lo que no esté firmado
Cuando un formato personalizado es inevitable, la guía indica que se pueden firmar los mensajes como parte del proceso de serialización y no deserializar ningún mensaje que no tenga una firma autenticada.
Lo importante es verificar antes de restaurar. Restaurar primero e inspeccionar después ya puede ser demasiado tarde, por la razón de arriba. El orden es todo el control.
Algunos mecanismos no se pueden proteger con configuración
Sobre BinaryFormatter de .NET, OWASP afirma sin rodeos que el tipo BinaryFormatter es peligroso y no se puede proteger.
Esa frase importa en la práctica: existen herramientas para las que «no pasa nada si se usa bien» simplemente no es cierto. Opciones de refuerzo, más validación, una lista de permitidos: nada de eso lo hace seguro. El único control es no usarlo.
Así que, al auditar, separa primero lo que la configuración puede proteger de lo que no. Dedicar esfuerzo a la segunda categoría no es mitigar; es aplazar.
Lo que menciona OWASP, por lenguaje
- PHP
- Evita
unserialize(); prefiere JSON - Python
pickle/c_pickle,loadde PyYAML yjsonpicklefiguran como peligrosos- Java
- Sobrescribe
ObjectInputStream#resolveClass()para limitar qué clases se pueden restaurar - .NET
BinaryFormatteres peligroso y no se puede proteger: no lo uses
Lo común: el mecanismo de persistencia integrado y cómodo de un lenguaje es justo lo que nunca debe recibir datos externos. Se creó para mover objetos completos entre partes que confían entre sí, y no contempla un remitente hostil.
API seguras e inseguras por lenguaje
La tabla reúne lo que la documentación oficial de cada lenguaje advierte que no uses con datos externos y lo que recomienda en su lugar. Si algo de la columna izquierda aparece en tu código, averigua de dónde vienen sus datos.
| Lenguaje | No usar con datos externos | Usar en su lugar | Qué dice la documentación oficial |
|---|---|---|---|
| Python | pickle.load / pickle.loads, marshal, yaml.load (con Loader=yaml.Loader) y yaml.unsafe_load, jsonpickle.decode | json.loads, yaml.safe_load | pickle «no es seguro»; firma con hmac si necesitas detectar manipulaciones |
| PHP | unserialize() (aunque se use allowed_classes) | json_decode() / json_encode() | No pases entradas no fiables, independientemente de allowed_classes |
| Ruby | Marshal.load | JSON, u otro formato que solo cargue tipos básicos | Cargar desde una fuente no fiable puede llevar a la ejecución remota de código |
| Java | ObjectInputStream#readObject, XMLDecoder, XStream#fromXML con datos externos | Pasar JSON a tus propias clases (DTO) | Si es inevitable, limita las clases permitidas con ObjectInputFilter |
| .NET | BinaryFormatter, SoapFormatter, NetDataContractSerializer, LosFormatter, ObjectStateFormatter, TypeNameHandling de Json.NET distinto de None | System.Text.Json, XmlSerializer, DataContractSerializer | Desde .NET 9, BinaryFormatter lanza una excepción al usarse |
Uno al lado del otro, la diferencia se ve así. La versión insegura convierte lo que llegó directamente en un objeto; la segura lo lee como valores y pasa solo los campos que necesitas a tu propio tipo.
# Evitar: convertir los bytes recibidos directamente en un objeto
order = pickle.loads(request_body)
# Usar: leer valores y pasar solo los campos necesarios a tu propio tipo
data = json.loads(request_body)
order = Order(id=int(data["id"]), quantity=int(data["quantity"]))En la versión segura, tu código (Order) decide qué clase se construye. Los datos que llegan solo deciden los valores de id y quantity, y la validación de entradas normal funciona sobre ellos.
Qué revisar en tu propio código
Enumera todos los puntos donde llegan por primera vez los datos externos
Cuerpos de peticiones, cookies, sesiones, colas, subidas, webhooks, cachés: todos los puntos donde algo se guarda y después se restaura. Las sesiones y las cachés son los olvidos más fáciles: datos que consideras tuyos pueden ser accesibles desde fuera según la ruta.
Averigua cuáles usan un formato que restaura tipos
Busca las funciones y clases mencionadas en la lista de arriba. Son lo primero que hay que eliminar. Si se usa un mecanismo que no se puede proteger (BinaryFormatter y similares), es la máxima prioridad: solo la migración lo resuelve.
Pasa a un formato de datos puro
Recibe JSON o XML y pásalo a tus propios tipos en tu propio código. Si aplicas validación de esquema en ese paso, cierras a la vez las claves inesperadas (la misma solución que para la contaminación de prototipos).
Donde no puedas cambiar, protégelo con una firma
Si el formato no puede cambiar, firma al serializar y verifica antes de restaurar. Haz que «sin firma, no se restaura» sea lo predeterminado. Trata la clave como se explica en qué tiene de peligroso el .env y las claves de API: nunca en el mismo lugar que el código.
Cuenta la restauración que hacen tus dependencias
Aunque tú no hayas escrito ninguna, una dependencia puede estar restaurando objetos. Siguen apareciendo CVE clasificados como CWE-502, así que ponlos bajo monitorización automática (primeros pasos con osv-scanner / un manual práctico para corregir CVE).
Cómo revisar tu propio entorno
Esto convierte la parte de «encontrarlo» de los pasos anteriores en órdenes y ajustes concretos. Todas las comprobaciones solo leen tu propio repositorio y tus propios servidores.
Busca en el texto del código fuente
Empieza por enumerar todos los candidatos. Con ripgrep (rg), las búsquedas son así (grep -rnE acepta los mismos patrones).
# Python
rg -n "pickle\.loads?\(|marshal\.loads?\(|yaml\.load\(|yaml\.unsafe_load|jsonpickle\.decode"
# PHP
rg -n "unserialize\("
# Ruby
rg -n "Marshal\.load"
# Java
rg -n "ObjectInputStream|readObject\(|XMLDecoder|fromXML\("
# .NET
rg -n "BinaryFormatter|SoapFormatter|NetDataContractSerializer|LosFormatter|ObjectStateFormatter|TypeNameHandling"No todos los resultados son peligrosos. Para cada uno, apunta de dónde vienen los datos que recibe y pon primero lo que esté conectado a una ruta accesible desde fuera: peticiones, cookies, subidas, almacenamiento compartido.
Busca avisos silenciados
En .NET, usar BinaryFormatter produce el aviso o error SYSLIB0011. Una configuración que lo silencia marca un lugar donde sigue habiendo una llamada peligrosa en el código.
rg -n "SYSLIB0011|CA23[0-3][0-9]" --glob "*.cs" --glob "*.csproj" --glob ".editorconfig" --glob "*.props"Si encuentras #pragma warning disable SYSLIB0011 o el ID dentro de <NoWarn>, confirma por qué está ahí y cuándo se migrará.
Activa reglas de análisis estático
En Python, las comprobaciones de Bandit relevantes son B301 (pickle y similares), B302 (marshal) y B506 (yaml.load); bandit -r . -t B301,B302,B506 ejecuta solo esas tres. En .NET, las reglas de análisis de código relevantes son la serie CA2300 (por ejemplo, CA2326, que señala TypeNameHandling de Json.NET). CA2326 no está activada por defecto ni siquiera en .NET 10, así que actívala en .editorconfig con una línea como dotnet_diagnostic.CA2326.severity = warning. Si las ejecutas en la CI, también se detienen las llamadas nuevas.
En Java, revisa los ajustes del filtro en tiempo de ejecución
Java puede limitar, en tiempo de ejecución, qué clases puede crear la deserialización (filtros de serialización). El mecanismo llegó con JDK 9 (JEP 290) y pasó a poder configurarse por contexto en Java 17 (JEP 415). En un proceso en ejecución, busca jdk.serialFilter en la salida de jcmd <PID> VM.system_properties. También se puede establecer como propiedad de seguridad en el archivo java.security, así que revisa ambos. Si no está en ninguno de los dos y tu código no llama a ObjectInputFilter.Config.setSerialFilter, no hay un filtro para todo el proceso.
Revisa las vulnerabilidades conocidas de las dependencias
Aunque tu propio código no tenga ninguna de estas llamadas, una dependencia puede restaurar objetos. Enumera las vulnerabilidades conocidas de tus dependencias con osv-scanner o una herramienta similar, y actualiza primero las clasificadas como CWE-502 (deserialización de datos no fiables).
Errores habituales y cómo corregirlos
| Error | Por qué es un problema | Corrección |
|---|---|---|
| Confiar en los datos porque «son internos» o «este archivo lo guardé yo» | Microsoft pone ejemplos de datos que cruzan una frontera de confianza sin que el desarrollador lo note: archivos guardados compartidos, sincronización en la nube, el equipo comprometido de un empleado | Si alguna ruta desde fuera puede llegar a ellos, trátalos como datos externos. No restaures nada sin firmar |
| Añadir una a una las clases peligrosas a una lista de bloqueo | No sirve contra combinaciones que no están en la lista o que aún no se conocen. El manual de PHP dice que no se pasen entradas no fiables ni siquiera con allowed_classes | Cambia el formato a JSON o similar. Solo donde sea inevitable, enumera las clases que permites |
| Restaurar primero y validar el contenido después | Durante la restauración se ejecuta código, así que la validación llega tarde | Pon la verificación de la firma antes de la restauración |
| Silenciar el aviso y darlo por corregido | Silenciar SYSLIB0011 o CA2326 deja la llamada peligrosa en su sitio | Busca los avisos silenciados y fija un plazo para la migración |
| Suponer que «es JSON, así que es seguro» | Los ajustes que permiten a JSON indicar sus propios tipos, como TypeNameHandling de Json.NET, lo convierten en un formato que elige tipos | Deja TypeNameHandling en None (el valor por defecto) |
Usar yaml.load con PyYAML | La documentación de PyYAML dice que yaml.load es tan potente como pickle y puede llamar a cualquier función de Python | Sustitúyelo por yaml.safe_load |
Un ejemplo real: restauración dentro de una extensión
El boletín de este sitio sobre CVE-2026-45247 trata una inyección de objetos PHP (CWE-502) en la extensión de Magento 2 «Mirasvit Full Page Cache Warmer» anterior a la 1.11.12. Permite la ejecución remota de código sin autenticación, tiene una puntuación CVSS de 9.3 y se añadió al catálogo de vulnerabilidades explotadas conocidas (KEV) de la CISA.
La lección es que el código de restauración no lo escribieron quienes gestionaban el sitio; estaba dentro de una extensión que instalaron. Por eso importa el paso de «revisar las dependencias» de arriba. La solución fue actualizar la extensión, y buscar en el propio código no lo habría encontrado.
La visión de este sitio: pregunta qué se le permite decidir a la entrada, no si el valor es válido
Cuando se dice «validación de entradas», la mayoría piensa en comprobar el valor: longitud, formato, rango. La amenaza de este artículo se resuelve antes de ese punto. Nuestro planteamiento: la pregunta real sobre una entrada no es «¿este valor es correcto?», sino «¿cuánto he dejado que decida esta entrada?».
¿Solo el valor? ¿También la estructura (contaminación de prototipos)? ¿También el tipo (este artículo)? ¿Incluso dónde termina (vulnerabilidades en la subida de archivos)? Cuenta las decisiones que has dejado en manos de fuera y los puntos peligrosos aparecen solos.
Fuentes (primarias)
- OWASP Cheat Sheet Series, "Deserialization Cheat Sheet" — cheatsheetseries.owasp.org (las tres consecuencias, el paso a un formato de datos puro, la firma para la integridad, las notas por lenguaje y la afirmación sobre
BinaryFormatterproceden de este documento; consultado el 5 de septiembre de 2026) - Documentación de Python, "pickle" — docs.python.org (el aviso de que «no es seguro»; firmar con
hmacy preferir JSON) - PyYAML Documentation — pyyaml.org (la diferencia entre
yaml.loadyyaml.safe_load) - Manual de PHP, "unserialize" — php.net (no pasar entradas no fiables independientemente de
allowed_classes; usar JSON) - Documentación de Ruby, "Marshal" — docs.ruby-lang.org
- Microsoft Learn, "Deserialization risks in use of BinaryFormatter and related types" — learn.microsoft.com (tipos igual de peligrosos, alternativas recomendadas, comportamiento desde .NET 9, ejemplos de cruce de una frontera de confianza)
- Microsoft Learn, "SYSLIB0011" y "CA2326" — SYSLIB0011 / CA2326
- OpenJDK, "JEP 290: Filter Incoming Serialization Data" y "JEP 415: Context-Specific Deserialization Filters" — JEP 290 / JEP 415
- Documentación de Bandit (B301, B302, B506) — bandit.readthedocs.io
- La tabla por lenguaje, los pasos de búsqueda y los errores habituales se contrastaron con los documentos anteriores el 6 de octubre de 2026
Leer a continuación
- La misma forma: contaminación de prototipos en Node.js (los datos externos deciden la estructura) / vulnerabilidades en la subida de archivos (los datos externos deciden dónde terminan las cosas)
- Glosario: qué es la ejecución remota de código (RCE)
- Ejemplo real: CVE-2026-45247 (inyección de objetos PHP en una extensión de Magento)
- Por framework: seguridad en ASP.NET Core / Spring Boot / Django
- Dependencias: primeros pasos con osv-scanner / un manual práctico para corregir CVE
FAQ
Q¿Por qué es tan peligrosa la deserialización? ¿No es solo una conversión de datos?
Porque en algunos formatos no es una conversión de datos, sino una reconstrucción de objetos. Cuando la información sobre qué clase instanciar viaja dentro de los datos, el atacante elige qué se construye. OWASP indica que se ha comprobado que los ataques contra deserializadores permiten ataques de denegación de servicio, de control de acceso o de ejecución remota de código. Lo que lo hace difícil de defender es que el ataque tiene éxito antes de que se ejecuten tus comprobaciones de valores.
Q¿Cuál es la defensa más fiable?
Limitar los datos de origen externo a un formato que no pueda elegir tipos. OWASP lo dice así: al pasar a un formato de datos puro como JSON o XML, se reduce la posibilidad de que una lógica de deserialización personalizada se reutilice con fines maliciosos. Si un formato personalizado es inevitable, la misma guía indica que se pueden firmar los mensajes como parte del proceso de serialización y no deserializar ningún mensaje sin una firma autenticada. Primero cambia el formato; si no puedes, átalo con una firma.
Q¿Puedo hacerlo seguro reforzando la configuración?
Depende del mecanismo. OWASP afirma sin rodeos que el tipo BinaryFormatter de .NET es peligroso y no se puede proteger. Hay mecanismos que no se vuelven seguros con ajustes ni con validación adicional: la única solución es dejar de usarlos. Separa lo que la configuración puede proteger de lo que no. Existen de verdad herramientas para las que «no pasa nada si se usa bien» no es cierto.
Q¿Qué debo vigilar en mi lenguaje?
Entre lo que OWASP menciona directamente: en PHP, evita unserialize() y prefiere JSON; en Python, pickle / c_pickle, load de PyYAML y jsonpickle son peligrosos; en Java, sobrescribe ObjectInputStream#resolveClass() para limitar qué clases se pueden restaurar; en .NET, no uses BinaryFormatter. Lo común es que el mecanismo de persistencia cómodo de un lenguaje es justo lo que no debes exponer a datos externos.
Q¿Cómo encuentro deserialización insegura en mi propio código?
En tres pasadas. Primero, busca en el texto con ripgrep o grep llamadas como pickle.loads, unserialize, Marshal.load, ObjectInputStream y BinaryFormatter. Segundo, activa reglas de análisis estático (B301, B302 y B506 de Bandit para Python; las reglas de análisis de código de la serie CA2300 para .NET). Tercero, analiza tus dependencias en busca de vulnerabilidades conocidas con una herramienta como osv-scanner y actualiza primero las de CWE-502. Para cada llamada que encuentres, apunta de dónde vienen los datos que recibe y corrige primero las que son accesibles desde fuera.
Q¿Si uso JSON, siempre es seguro?
No si activas un ajuste que permite a los datos indicar sus propios tipos. El ejemplo habitual es TypeNameHandling de Json.NET en .NET; la regla de análisis de código CA2326 de Microsoft dice que no se use ningún valor distinto de None. La ventaja de JSON es que solo transporta valores, así que el código que lo recibe decide qué clase se construye. Comprueba que no has activado un ajuste que elimine esa ventaja.