Situación actual: PHP2Shell. Dos vulnerabilidades críticas de PHP (CVSS 9.2) permiten ejecutar código malicioso sin autenticación. Parches desde el 21/10/2026.Qué hacer ahora →

Deserialización insegura en PHP: unserialize y seguridad

unserialize() con datos ajenos puede llevar, en el peor caso, a la ejecución de código. Cómo sustituirlo por JSON, restringirlo donde deba quedarse y encontrar puntos de riesgo en el código.

Actualizado 6 min de lecturaGuías

En resumen: en la seguridad de unserialize en PHP rige una regla sencilla: nunca procese con unserialize() datos que vengan de fuera. Use JSON en su lugar. Donde unserialize() deba quedarse por código heredado, límitelo con allowed_classes => false y compruebe la autenticidad de los datos con una firma.

Qué puede salir mal con unserialize()

PHP puede convertir arrays y objetos en una cadena de texto con serialize() y restaurarlos con unserialize(). Es cómodo para guardar datos en cookies, cachés o campos de la base de datos. El problema: unserialize() también crea objetos de cualquier clase que pueda cargarse en la aplicación.

Al crear un objeto así y, a más tardar, al destruirlo, PHP llama automáticamente a ciertos métodos, como __wakeup(), __unserialize() o __destruct(). Si las clases del proyecto o de las bibliotecas tienen esos métodos con acciones sobre archivos, bases de datos o el sistema, un atacante puede desviarlos de su finalidad mediante datos manipulados. Los especialistas lo llaman PHP Object Injection. Según las clases existentes, el efecto va desde borrar archivos sueltos hasta ejecutar código malicioso.

Resumido para quien no es técnico: una sola llamada descuidada en el lugar equivocado puede dejar entrar a un atacante muy adentro de la aplicación. Por eso esta debilidad forma parte desde hace años de los temas del OWASP Top 10.

La alternativa segura: JSON en lugar de unserialize

Para casi todos los casos en que los datos llegan de fuera o salen del servidor, JSON es la elección correcta. json_decode() solo crea arrays, números, cadenas y valores booleanos, no objetos de clases propias.

<?php
declare(strict_types=1);

function leseEinstellungen(string $eingabe): array
{
    try {
        $daten = json_decode($eingabe, true, 16, JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        throw new InvalidArgumentException('Ungültige Daten.');
    }

    if (!is_array($daten)) {
        throw new InvalidArgumentException('Erwartet wurde ein Objekt.');
    }

    // Aceptar solo campos conocidos con los tipos esperados
    return [
        'sprache' => in_array($daten['sprache'] ?? '', ['de', 'en'], true) ? $daten['sprache'] : 'de',
        'seiten'  => max(1, min(100, (int) ($daten['seiten'] ?? 20))),
    ];
}

Lo importante es la segunda parte: también los datos JSON son entradas. Acepte solo los campos esperados y compruebe los tipos y los rangos de valores antes de trabajar con ellos.

Cuando unserialize es inevitable: allowed_classes y firma

En las aplicaciones más antiguas, los datos serializados suelen estar en bases de datos o cachés que no se pueden migrar de inmediato. Entonces se limita el riesgo en dos pasos. Primero, con allowed_classes se impide que se creen objetos. Segundo, con una firma HMAC se garantiza que los datos proceden realmente de su aplicación y no se han modificado por el camino.

<?php
declare(strict_types=1);

const SCHLUESSEL = 'aus-sicherer-konfiguration-laden'; // nunca en el repositorio de código

function verpacken(array $daten): string
{
    $roh = serialize($daten);
    return hash_hmac('sha256', $roh, SCHLUESSEL) . '.' . base64_encode($roh);
}

function auspacken(string $paket): array
{
    [$signatur, $b64] = array_pad(explode('.', $paket, 2), 2, '');
    $roh = base64_decode($b64, true);

    if ($roh === false || !hash_equals(hash_hmac('sha256', $roh, SCHLUESSEL), $signatur)) {
        throw new RuntimeException('Signatur ungültig.');
    }

    $daten = unserialize($roh, ['allowed_classes' => false, 'max_depth' => 16]);
    if (!is_array($daten)) {
        throw new RuntimeException('Unerwartetes Format.');
    }
    return $daten;
}

Si de todos modos se necesitan determinadas clases, nómbrelas una a una en allowed_classes en lugar de permitirlas todas. La clave pertenece a la configuración del servidor o a un almacén de secretos y debe renovarse tras un incidente.

Migrar gradualmente los datos existentes a JSON

En aplicaciones crecidas, los valores serializados suelen llevar años en la base de datos. Una migración de una sola vez es entonces arriesgada. Ha dado buen resultado una transición: la aplicación lee ambos formatos, pero solo escribe JSON. Al leer un valor antiguo, se desempaqueta una vez con allowed_classes => false y se vuelve a escribir enseguida como JSON. Pasado un tiempo, la mayoría de las entradas están migradas, y un script puntual se encarga del resto.

Registre durante la transición con qué frecuencia se sigue leyendo el formato antiguo y también cuándo las firmas no coinciden. Esos fallos pueden indicar intentos de manipulación y pertenecen a la monitorización. Solo cuando ya no aparezca ningún formato antiguo, elimine por completo del código la rama de unserialize().

Deserialización oculta: sesiones, cachés y rutas de archivos

No toda deserialización aparece de forma visible en el código propio. Revise también estos puntos:

  • Sesiones: PHP serializa automáticamente los datos de sesión. Mientras los atacantes no puedan escribir en los archivos de sesión, no es crítico. Las rutas de sesión separadas por sitio web las describe la guía Proteger las sesiones en PHP.
  • Serializador de sesiones: el aviso sobre CVE-2026-115560 afecta a una configuración poco habitual, en la que el serialize_handler se cambia en tiempo de ejecución mediante ini_set() y el progreso de subida de sesión está activo. Mitigación según php.net: session.upload_progress.enabled=0 en php.ini o en el pool de FPM, y después reiniciar los workers.
  • Cachés y colas: Redis, Memcached o tablas de base de datos con valores serializados son tan seguros como el acceso a ellos. No deben estar abiertos a la red.
  • Funciones de archivos con rutas procedentes de entradas: no pase nunca a las funciones de archivos rutas procedentes de la petición. Los stream wrappers como phar:// pueden, de otro modo, provocar un procesamiento inesperado. Una lista de permitidos para los nombres de archivo protege aquí con más fiabilidad que cualquier filtrado.
  • Dependencias: las bibliotecas con vulnerabilidades conocidas amplían las posibilidades de un atacante. Manténgalas actualizadas, como se describe en la guía Revisar vulnerabilidades en las dependencias de Composer.

Encontrar puntos de riesgo en el código

Un primer inventario se logra con una búsqueda sencilla. No cambia nada en el sistema y muestra todas las llamadas fuera del directorio vendor:

grep -rn --include='*.php' 'unserialize(' /var/www/projekt \
  | grep -v '/vendor/'

Valore cada coincidencia con tres preguntas: ¿de dónde vienen los datos?, ¿puede un usuario modificarlos?, ¿está establecido allowed_classes? Tienen prioridad las coincidencias con entradas de cookies, $_GET, $_POST, $_REQUEST o interfaces externas. Sustitúyalas por JSON y pruebe la migración antes de desplegarla.

Otro indicio está en el navegador. Los datos serializados de PHP tienen una estructura típica con identificadores de tipo e indicaciones de longitud. Si encuentra valores así en cookies, campos de formulario ocultos o parámetros de su propio sitio web, la aplicación ha entregado datos serializados al visitante y probablemente los espera de vuelta. Justo esos puntos son los que hay que revisar.

Para la dirección basta una pregunta sencilla a los desarrolladores o a la agencia: «¿Procesa nuestra aplicación en algún lugar datos serializados que vengan de fuera y, en caso afirmativo, cómo están protegidos?» Una respuesta clara con los puntos concretos es una buena señal. Una respuesta evasiva es motivo para hacer revisar la aplicación.

¿Cómo ayuda PHP SOS en concreto?

  • Buscamos en su código la deserialización con datos ajenos y valoramos cada hallazgo según su accesibilidad y su impacto.
  • Proponemos migraciones concretas a JSON o llamadas protegidas y acompañamos a sus desarrolladores en la aplicación.
  • Revisamos las dependencias en busca de vulnerabilidades conocidas y documentamos el estado.
  • Si lo desea, hacemos revisar la aplicación de forma independiente en el marco de una auditoría de seguridad y prueba de penetración.

Preguntas frecuentes

¿Es unserialize() en PHP inseguro por principio?

No para datos que solo genera su propia aplicación y que nadie puede modificar. Se vuelve peligroso en cuanto usuarios o terceros pueden influir en la entrada, por ejemplo a través de cookies, campos de formulario o interfaces externas.

¿Basta allowed_classes => false como protección?

Impide que se creen objetos arbitrarios y cierra así la vía de ataque más importante. Para entradas externas, JSON sigue siendo la mejor opción, porque el analizador es más sencillo y no reconstruye objetos.

¿Ayuda actualizar las bibliotecas contra los ataques de deserialización?

En parte. Los ataques suelen usar clases de dependencias que ejecutan acciones al destruir o despertar un objeto. Los paquetes actuales cierran las cadenas conocidas, pero la causa real es la llamada a unserialize con datos ajenos.

¿Tiene PHP2Shell algo que ver con la deserialización?

Las dos vulnerabilidades principales residen en el procesamiento de peticiones de PHP. El aviso sobre CVE-2026-115560, en cambio, afecta a una configuración poco habitual de la serialización de sesiones. La mitigación allí es session.upload_progress.enabled=0.

Fuentes

Contacto inmediato

Cada hora cuenta. Escríbanos ahora.

Describa brevemente el sistema, la versión de PHP y lo que ha observado. Nos pondremos en contacto, aclararemos la situación y trabajaremos en remoto hasta que el incidente quede cerrado y documentado.

Ayuda inmediata