Skip to main content

Cómo prevenir la deserialización insegura en Node.js

blog feature playwright tests

17 de abril de 2023

0 minutos de lectura

Nota del editor: 31 de mayo de 2023

Esta publicación se actualizó para mostrar cómo Snyk Open Source y la extensión de Snyk para VS Code pueden ayudarte a prevenir la deserialización insegura en tus proyectos.

La serialización es el proceso de convertir un objeto de JavaScript en un flujo de bytes secuenciales para enviarlo por una red o guardarlo en una base de datos. La serialización cambia el formato de los datos originales y conserva su estado y propiedades, para que podamos recrearlos cuando sea necesario. Gracias a la serialización, podemos escribir datos complejos en archivos, bases de datos y memoria entre procesos, y enviar esos datos complejos entre componentes y a través de redes.

La deserialización es lo opuesto a la serialización: convierte los datos serializados nuevamente en un objeto con el mismo estado que tenía antes. El problema es que la deserialización puede ser insegura y exponer datos confidenciales. La causa principal de la deserialización insegura es no proteger la deserialización de las entradas del usuario. La mejor manera de evitarla es asegurarnos de deserializar únicamente datos validados y depurados. 

Los atacantes pueden aprovechar esta vulnerabilidad al cargar código malicioso en un objeto serializado que parece inofensivo y enviarlo a una aplicación. Si no hay verificaciones de seguridad adicionales, la aplicación web deserializa el objeto recibido y ejecuta el código malicioso. Como alternativa, un atacante puede extraer datos confidenciales incluidos en un objeto serializado.

Las consecuencias de la serialización insegura pueden ser graves. Por ejemplo, cuando un sitio web deserializa datos de forma insegura, los hackers pueden manipular objetos serializados e introducir datos dañinos en el código de una aplicación. Incluso pueden enviar un objeto serializado completamente diferente para acceder a otras partes de la aplicación, lo que la deja expuesta a más ataques y a la pérdida de datos.

Deserialización insegura en Node.js

node-serialize y serialize-to-js son paquetes de Node.js y JavaScript vulnerables a la deserialización insegura. A diferencia de JSON.parse y JSON.stringify, que solo serializan objetos en formato JSON, estas dos bibliotecas serializan objetos de casi cualquier tipo, incluidas las funciones. Esta característica las hace vulnerables a la contaminación de prototipos, un ataque de inyección en el que un actor malicioso obtiene el control de los valores predeterminados de las propiedades de un objeto.

Si un ataque de contaminación de prototipos tiene éxito, el atacante puede causar más daños al alterar la lógica de la aplicación, iniciar la ejecución remota de código o provocar un ataque de denegación de servicio (DoS).

Este artículo muestra cómo corregir vulnerabilidades de deserialización en Node.js. Crearemos código vulnerable, mostraremos un ataque y luego corregiremos las vulnerabilidades.

Requisitos previos

Para seguir este tutorial, necesitarás:

  • Tener Node.js instalado

  • Conocimientos de JavaScript

Cómo corregir vulnerabilidades de deserialización en Node.js

Empecemos por configurar un proyecto de Node.js. Ejecuta el siguiente comando:

npm init -y

Instala el framework de aplicaciones web Express:

npm install express

Instala el paquete de serialización de Node.js:

npm install node-serialize

Instala un analizador de cookies:

npm install cookie-parser

Crea un archivo nuevo server.js y agrega el siguiente código:

var express = require('express');
var cookieParser = require('cookie-parser');
var escape = require('escape-html');
var serialize = require('node-serialize');
var app = express();
app.use(cookieParser())

app.get('/', function(req, res) {
	if (req.cookies.profile) {
		var str = new Buffer(req.cookies.profile, 'base64').toString();
		var obj = serialize.unserialize(str);
		if (obj.username) {
			res.send("Hello " + escape(obj.username));
		}
	} else {
		res.cookie('profile', "eyJ1c2VybmFtZSI6IkpvaG4iLCJnZW5kZXIiOiJNYWxlIiwiQWdlIjogMzV9", {
			maxAge: 900000,
			httpOnly: true
		});
	}
	res.send("Welcome to the Serialize-Deserialize Demo!");
});
app.listen(3000);
console.log("Listening on port 3000...");

En el archivo anterior, usamos el módulo inseguro node-serialize. También pasamos datos no confiables a su función, unserialize().

¿Cuántos problemas de seguridad puedes detectar en el código anterior? ¿Cómo sabrías si el código o las dependencias que agregamos al proyecto tienen problemas de seguridad? 

Aquí es donde entra en juego la extensión de Snyk Code. Es gratis y rápida de poner en marcha. Ve a Extensiones en VS Code (SHIFT+CMD+X), busca Snyk, instálala y únete a más de 90 000 desarrolladores.

Te mostraré una captura de pantalla de Snyk Code en acción con el fragmento de código de la aplicación Express anterior:

Captura de pantalla que muestra varios posibles problemas de seguridad en el servidor de aplicaciones Express.

Observa las flechas moradas y cómo señalan posibles problemas de seguridad en este servidor de aplicaciones Express. Si observas con atención, encontrarás más problemas y notarás que Snyk Code también anota las líneas de código del archivo `server.js` para mostrar cómo el código inseguro fluye entre distintas rutas y archivos de código.

Obtén la extensión de Snyk para VS Code. ¡Es gratis!

Volvamos a la aplicación web de Node.js. El resultado es que el endpoint app.get('/', function(req, res) queda expuesto a la deserialización insegura. Esto se debe a que la aplicación agrega las entradas de los usuarios al valor de una cookie de preferencias que ya está serializada. 

Para demostrar la vulnerabilidad, ejecuta el servidor de Express con este comando:

node server.js

Esto inicia el servidor en el puerto 3000. Abre http://localhost:3000/ para verlo e inspeccionar las cookies, como se muestra a continuación:

Las herramientas para desarrolladores del navegador muestran la pestaña Application con una cookie de localhost llamada “profile2” y su valor codificado.

Ahora, convierte la cookie del formato base64 al formato JSON. Puedes hacerlo con una herramienta en línea como Code Beautify. Esta es la cookie en base64:

eyJ1c2VybmFtZSI6IkpvaG4iLCJnZW5kZXIiOiJNYWxlIiwiQWdlIjogMzV9

Y esta es la cookie que configura el servidor después de convertirla de base64 a JSON:

{"username":"John","gender":"Male","Age": 35}

Para ver el problema de deserialización en acción, cambiemos el valor del objeto JSON y codifiquémoslo en base64. Luego, reemplazaremos el valor de la cookie en el navegador por la nueva cookie.

Cambia el objeto JSON por:

{"username":"Joe Jones","gender":"Male","Age": 40}

Abre esta herramienta de Base64 para codificar el JSON. Al codificarlo en base64, queda así:

eyJ1c2VybmFtZSI6IkpvZSBKb25lcyIsImdlbmRlciI6Ik1hbGUiLCJBZ2UiOiA0MH0=

Ahora, reemplaza el valor actual de la cookie en el navegador por el valor codificado del nuevo objeto y edita el valor de la respuesta HTTP en el archivo server.js de la siguiente manera:

res.send("Welcome to the Insecure Deserialize Demo!");

Al reiniciar la conexión y actualizar la página, verás la nueva respuesta.

Cómo aprovechar la vulnerabilidad

Aprovechemos la vulnerabilidad para ejecutar código arbitrario (ACE), que pasa datos no confiables a la función unserialize(). 

Primero, crearemos una carga útil con la función serialize() del mismo módulo con el que hemos estado trabajando. Para hacerlo, crea un archivo llamado serialize.js y agrega el siguiente código:

var serialize = require('node-serialize');
var m = {
	myOutput: function() {
		return 'Hello';
	}
}
console.log("Serialized: \n" + serialize.serialize(m) + "\n");

Este es el script de Node.js que serializará nuestro código. 

Ahora, abre una terminal, ve a la carpeta raíz y ejecuta este comando:

node serialize.js

Reemplaza el parámetro username en el valor de la cookie por el valor del parámetro myOutput que aparece en el resultado anterior. Nuestro nuevo objeto en la cookie debería verse así:

{"username":"_$$ND_FUNC$$_function(){ return 'Hello'; }","gender":"Male","Age": 40}

El resultado anterior nos da una carga útil serializada para pasar a la función unserialize(). Para lograr ACE, tenemos que usar una expresión de función ejecutada inmediatamente (IIFE) de JavaScript para llamar a la función.

Al agregar los paréntesis de la IIFE, (), después del cuerpo de la función de esta carga útil serializada, la función se ejecutará después de crear el objeto. A continuación se muestra el objeto JSON que ahora se almacenará en la cookie.

{
	"username": "_$$ND_FUNC$$_function(){ return 'Hello'; }()",
	"gender": "Male",
	"Age": 40
};

Para mostrar cómo este objeto manipulado introduce la evaluación dinámica de código en un servidor Node.js en ejecución, lo usaremos como ejemplo en nuestro archivo de utilidad serialize.js. Pasemos esta carga útil a una función unserialize(). Modifica el archivo seralize.js agregando el siguiente código:

var serialize = require('node-serialize');
var m = {
	myOutput: function() {
		return 'Hello';
	}
}

console.log("Serialized: \n" + serialize.serialize(m) + "\n");

var km = {
	"username": "_$$ND_FUNC$$_function(){ return 'Hello'; }()",
	"gender": "Male",
	"Age": 40
};
console.log(serialize.unserialize(km));

En el código anterior, agregamos una nueva variable, km, que contiene una función como valor de username y que pasamos a la función unserialize().

Ahora, ejecuta el archivo de script de utilidad con el siguiente comando para comprobar si se ejecuta la función:

node serialize.js

Deberías obtener el siguiente resultado:

Ejemplo de código que muestra datos de JavaScript serializados y el resultado de deserializarlos, con los campos de nombre de usuario, género y edad.

¡El proceso de serialización se aprovechó con éxito! Como atacantes que controlan la entrada del objeto del lado del cliente, pudimos manipularlo para incluir código JavaScript que cambia el campo de nombre de usuario.

Cómo corregir la vulnerabilidad de deserialización insegura

La mejor manera de prevenir la deserialización insegura es evitar por completo la deserialización de entradas del usuario. La segunda mejor opción es verificar las entradas del usuario antes de serializarlas. Podemos usar un paquete llamado Serialize JavaScript para depurar las entradas del ejemplo vulnerable anterior.

Primero, instala serialize-javaScript con npm:

npm install serialize-javascript

Modifica el archivo serialize.js y reemplaza su código por este:

var serialize = require('serialize-javascript');
var m = {
	myOutput: function() {
		return 'Hello';
	}
}
console.log("Serialized: \n" + serialize(m, {
	ignoreFunction: true
}) + "\n");

ignoreFunction garantiza que no se serialicen las funciones que se usan para ejecutar ACE. Ejecuta el archivo para confirmarlo:

node serialize.js

Este es el resultado:

Fragmento de código que muestra «Serialized:» seguido de un objeto vacío, «{}».

Asegurémonos de que el nuevo serialize-javascript sugerido en este artículo no tenga vulnerabilidades, según los informes de CVE. En el comando npm ls que aparece a continuación, puedes ver que se agregó al proyecto junto con el paquete vulnerable node-serialize:

$ npm ls
b@1.0.0 /private/tmp/b
├── cookie-parser@1.4.6
├── express@4.18.2
├── node-serialize@0.0.4
└── serialize-javascript@6.0.1

Luego, lo analizamos con el paquete gratuito de npm snyk:

$ snyk test

Testing /private/tmp/b...

Tested 62 dependencies for known issues, found 1 issue, 1 vulnerable path.

Issues with no direct upgrade or patch:
  ✗ Arbitrary Code Execution [Critical Severity][https://security.snyk.io/vuln/npm:node-serialize:20170208] in node-serialize@0.0.4
    introduced by node-serialize@0.0.4
  No upgrade or patch available

Organization:      lirantal
Package manager:   npm
Target file:       package-lock.json

Esto muestra que el paquete de npm node-serialize es vulnerable, como demostramos en este artículo, y que no hay vulnerabilidades de seguridad conocidas públicamente en el paquete de npm recomendado serialize-javascript.

Te recomendamos encarecidamente comenzar a usar Snyk Open Source y usar el paquete de npm snyk como parte de tu flujo de trabajo habitual de desarrollo. Asegúrate de agregarlo a tu pipeline de CI/CD para evitar que aparezcan nuevas vulnerabilidades de seguridad.

Prácticas recomendadas para evitar vulnerabilidades de deserialización

La deserialización de entradas del usuario puede permitir que actores maliciosos ataquen un sistema y expongan datos confidenciales. La mejor manera de protegerse contra la deserialización insegura es evitar deserializar datos de fuentes no confiables. Pero si necesitamos deserializar datos, debemos implementar medidas de seguridad adicionales para verificar que no se hayan manipulado.

Además, debemos depurar las entradas. Podemos hacerlo comprobando si el objeto que se va a serializar contiene funciones y deteniendo la serialización si las contiene.

Deserialización segura

La serialización y la deserialización son procesos fundamentales que permiten transferir datos sin problemas a través de una red. Sin embargo, la deserialización insegura puede provocar vulnerabilidades críticas. Al inyectar objetos serializados maliciosos en una aplicación web, por lo general en forma de datos ingresados por el usuario que luego se deserializan, los atacantes pueden introducir datos dañinos en una aplicación. Después, pueden lanzar un ataque de inyección, ejecutar código de forma remota o realizar un ataque DDoS.

Podemos prevenir los ataques causados por esta vulnerabilidad si simplemente evitamos deserializar las entradas del usuario. Si es necesario deserializar, debemos incluir mecanismos de seguridad adicionales, como tokens antifalsificación, para garantizar que los datos no se hayan modificado.

Visita el blog de Snyk para obtener más información sobre cómo prevenir vulnerabilidades de serialización y deserialización inseguras en Java.

Para proteger tu código contra vulnerabilidades de serialización y deserialización inseguras, agrega el plugin de Snyk.io a tu IDE.

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.