Un fallo crítico en PhpSpreadsheet se saltó su propio parche

Hace unas semanas, el equipo de PHPOffice publicó una corrección para un fallo de deserialización phar en PhpSpreadsheet, registrado como CVE-2026-34084. Los investigadores encontraron luego la manera de eludir esa corrección, y el bypass es ahora su propia vulnerabilidad: CVE-2026-45034, con una puntuación crítica de 9,2 en CVSS v4. El aviso, GHSA-87m4-826x-3crx, se hizo público el 7 de junio. Si PhpSpreadsheet lee archivos de hoja de cálculo en algún punto de su stack, este asunto merece diez minutos de su atención hoy.
El parche original añadió un helper llamado File::prohibitWrappers cuyo objetivo era rechazar los stream wrappers como phar://. Para ello llamaba a parse_url($filename, PHP_URL_SCHEME) y lanzaba una excepción si el esquema devolvía una cadena de más de un carácter. El problema es que esa comprobación no equivale a «¿usa esta ruta un wrapper?». Si se le pasa una ruta con la forma phar:///path/file.phar/inner, con tres barras tras el esquema, parse_url devuelve el booleano false en lugar de la cadena del esquema. La comprobación se omite, el helper retorna sin quejarse y IOFactory::load() accede igualmente al wrapper phar.
Por qué la versión de PHP importa más que la puntuación CVSS
Lo que ocurre después depende por completo de su versión de PHP, y este es el punto que los equipos suelen leer mal. En PHP 7.x, con solo acceder al wrapper phar mediante is_file es suficiente para que PHP deserialice los metadatos del phar, lo que dispara __wakeup y __destruct sobre objetos controlados por el atacante. Eso es ejecución remota de código completa. Los investigadores lo demostraron en la rama 1.x bajo PHP 7.4 con una cadena de gadgets que escribía un archivo marcador. En PHP 8.x, la deserialización automática de metadatos para operaciones de archivo simples fue eliminada, así que en la capa de PhpSpreadsheet el fallo se reduce a una primitiva de lectura de archivos phar. La ejecución de código solo reaparece si algo en la parte inferior de su aplicación llama a Phar::getMetadata sobre esa ruta.
Léalo de esta manera. ¿Sigue en PHP 7.x? Es crítico e inmediato. En PHP 8.x no está automáticamente a salvo: está a una sola llamada a Phar::getMetadata del mismo resultado, y una lectura arbitraria de archivos phar tampoco es un fallo que quiera tener en un endpoint de importación de documentos.
Qué nos dice esto sobre cómo se eluden los parches
En nuestros trabajos de pentesting, la deserialización phar es una de las primeras cosas que evaluamos cuando una aplicación recibe una ruta de archivo, un nombre de archivo o una subida y lo pasa a una biblioteca. La parte interesante de este CVE no es el truco del wrapper, sino la forma que tenía la corrección original. El parche validaba el esquema que devolvía parse_url, cuando la regla real era «esta ruta no debe referenciar ningún wrapper». Corrija el síntoma visible y dejará abiertos los casos que no probó. Tres barras fue todo lo que hizo falta.
Vemos el mismo reflejo fuera de PHP. En nuestros proyectos con PHP y Docker para construir aplicaciones cloud-native para clientes empresariales, las bibliotecas que analizan entradas no confiables son las que merecen una segunda revisión en cada actualización de dependencias, no solo cuando aparece un CVE. En nuestros proyectos nativos de iOS y Android el patrón se repite con SDKs de terceros que aceptan silenciosamente una URL o una ruta. El equipo confía en la biblioteca, la biblioteca confía en la entrada, y nadie es responsable del límite intermedio. Un nuevo parche no es el fin de una clase de vulnerabilidad, es un dato más sobre dónde se encuentra realmente ese límite.
Qué hacer esta semana
Tres pasos concretos, en orden:
- Actualice PhpSpreadsheet. La corrección es la 1.30.5 en la rama 1.x, o la versión parcheada de la rama que siga: 2.1.17, 2.4.6, 3.10.6 o 5.8.0. El nuevo código elimina el truco de
parse_urly utiliza patrones explícitos para identificarphar://y wrappers similares. - Busque en su código
IOFactory::loadyReader::load, y rastree cada llamada hasta su origen. Cualquiera que sea accesible desde un nombre de archivo proporcionado por el usuario, un nombre de subida o una URL es la que importa. Fije la versión parcheada antes de que ese endpoint vuelva a estar en producción. - Añada defensa en profundidad en la capa de PHP. Deshabilite los stream wrappers
phar://,php://,data://yexpect://donde se ejecute su código de importación, y valide o canonicalice cualquier ruta antes de que llegue a un cargador. Si sigue en PHP 7.x, este CVE es un motivo más para completar esa migración. El propio runtime eliminó el sumidero de deserialización automática en PHP 8.
Si desarrolla en PHP, un hábito útil es tratar cada biblioteca de manejo de archivos como hostil a rutas no confiables por defecto, tal como hacemos cuando desarrollamos y revisamos servicios backend para clientes con requisitos reales de cumplimiento normativo. El coste de ese hábito son unos minutos por dependencia. El coste de omitirlo es un RCE en su pipeline de documentos.
Si una biblioteca de terceros que accede silenciosamente a phar:// en su stack de PHP le parece un problema que preferiría detectar antes que un atacante, hablemos.