Si tu empresa tiene una web o una aplicación, esto probablemente va contigo. Habla hoy mismo con tu equipo técnico o con quien te mantenga la web y pregúntale si ya está al tanto de estas vulnerabilidades. Más vale una llamada hoy que un susto mañana.

Parece que estamos en lo que algunos ya llaman el octubre negro de la seguridad. Y la verdad es que, con todo lo que ha pasado estos días, no es exagerado. Y parece que va a ser la tónica general en tiempos de IA.

El martes por la tarde nos enteramos por un tuit de que PHP, el lenguaje sobre el que funcionan WordPress y buena parte de las webs del mundo, iba a publicar al día siguiente un parche para dos vulnerabilidades graves. Sin detalles, solo una puntuación de 9,2 sobre 10 y una mala noticia: las versiones antiguas no tendrían arreglo. Tenemos bastantes WordPress a nuestro cargo, así que nos pusimos en alerta (y a sudar).

Ese fue el primer aviso, pero no el último. El miércoles le tocó a Node.js, el motor de muchísimas aplicaciones y APIs. Y casi a la vez salió una investigación que explicaba cómo tumbar servidores que usan HTTP/3, la versión más moderna del protocolo con el que se sirven las webs. Y, por si fuera poco, se supo que eMudhra, una de las entidades que garantizan que una web es quien dice ser, tenía unas claves tan débiles que cualquiera podía fabricar certificados falsos para cualquier dominio.

Cuatro frentes en dos días. En este post os contamos cómo hemos afrontado una crisis tan seria con la ayuda de agentes de IA, qué decisiones hemos tomado nosotros y qué hemos aprendido. La primera parte es para todo el mundo. La segunda, para los técnicos.

Qué está en juego

Sin entrar en tecnicismos: las de PHP y Node.js permiten, en el peor de los casos, que alguien desde fuera ejecute código en un servidor sin necesitar usuario ni contraseña. Es decir, quedarse con él. La de HTTP/3 sirve para dejar un servidor sin memoria y, por tanto, fuera de servicio. Y la de eMudhra permite hacerse pasar por webs legítimas, por ejemplo para colar paquetes de software falsos al instalar programas en Python.

Lo más delicado no han sido los servidores al día, que se arreglan actualizando, sino los antiguos. Cuando una versión deja de tener soporte, no recibe parches. Y entonces solo quedan dos caminos: migrar o apagar.

Agentes que conocen la casa

Llevamos tiempo trabajando con agentes que conocen el inventario de nuestra infraestructura. Tienen acceso limitado y permisos expresos para tareas concretas, nada de barra libre.

Y hay otra pieza que lo hace posible: nuestra infraestructura está escrita en código. Cómo se configura cada servidor, qué versiones lleva o cómo se despliega cada aplicación no depende de que alguien se acuerde de qué tocó hace años. Todo está en ficheros, versionado y revisado como cualquier otro código. Así que cambiar algo es modificar unas líneas, probarlas y aplicarlas. Y para un agente, leer y cambiar código es justo lo que mejor se le da.

El reparto de trabajo ha sido el mismo en todos los casos. Los agentes han hecho lo pesado: revisar cada servidor, leer los avisos y compararlos con nuestro código, sacar copias de seguridad, comprobarlas, aplicar lo que íbamos aprobando y dejarlo todo documentado. Nosotros hemos decidido qué se tocaba, qué se apagaba, qué se publicaba y en qué orden.

Y el orden, el de siempre. Primero copias de seguridad, verificadas una a una. Después, probar en local, luego en un entorno de pruebas y por último en producción. Un servidor cada vez, comprobando antes y después.

Media hora después del parche

El miércoles lo pasamos con un ojo en las noticias y otro en Twitter, contrastando lo que publicaban PHP, los proveedores y los organismos oficiales, tanto internacionales como españoles. Cuando salió el parche, poco antes de las cuatro de la tarde, ya lo teníamos todo preparado.

Media hora después de publicarse, todos nuestros servidores de producción estaban actualizados. Hubo un único contratiempo: uno de los servidores de pruebas vio tanta actividad nuestra que decidió que éramos un ataque y nos cerró la puerta durante más de treinta minutos. Las medidas de seguridad que tenemos configuradas, funcionando de maravilla contra nosotros mismos.

Con Node.js y HTTP/3 ha sido algo parecido. Antes de correr, los agentes han revisado por dónde podía entrar el problema en cada aplicación. Hoy hemos actualizado lo que hacía falta, cada proyecto con su forma de desplegar.

Con eMudhra el riesgo está, en nuestro caso, sobre todo en la parte de desarrollo, que es donde se instalan paquetes de Python. Hemos preparado con un agente unas instrucciones que revisan un ordenador sin tocar nada y, cuando les das el visto bueno, lo corrigen en el orden correcto. Las hemos publicado para que cualquiera pueda usarlas y se las hemos pasado también a algunos clientes.

Donde está el barro

Lo complicado fueron las máquinas antiguas, con versiones de PHP que ya no tenían soporte. Hicimos copia y las apagamos.

Una de las webs que alojaban la migramos ese mismo día a un servidor nuevo, actualizando de paso el diseño, los plugins y todo lo que llevaba años sin tocarse. Los agentes hicieron la migración de forma autónoma y nosotros fuimos revisando y tomando las decisiones. Las demás siguen su proceso con cada cliente.

Teatros Ejemplares: de WordPress a HTML

El caso más interesante es el de Teatros Ejemplares. Es la publicación digital del homenaje a Cervantes y sus Novelas ejemplares que hicieron dramaturgos actuales de España y América Latina.

El contenido no había cambiado desde 2016. Pero ahí estaba, en un WordPress con 21 plugins, y cada vulnerabilidad nueva nos obligaba a parchear algo que nadie editaba. La verdad es que era mantener una web viva para que hiciera de archivo.

Así que aprovechamos para cortar por lo sano y convertirla en una web completamente estática: páginas HTML de toda la vida. Sin PHP, sin base de datos y sin nada que parchear.

Un agente recorrió la web entera y guardó cada página tal cual se ve, con las mismas direcciones, para no romper enlaces ni perder posicionamiento. Luego la limpiamos: fuera el Google Analytics que llevaba años sin funcionar y fuera el banner de cookies, que solo estaba ahí por él. En su lugar, una analítica que no usa cookies. Comprobamos que no había ni un enlace roto y la publicamos con el dominio de siempre.

Del diseño de la migración a la web lista para publicar pasaron unas dos horas. Y el WordPress no lo hemos borrado: sigue guardado como vuelta atrás, por si acaso.

Para una web que no cambia, lo más seguro es que no haya nada que ejecutar.

Más de una semana en dos jornadas

A mano, todo esto nos habría llevado más de una semana. Con agentes y supervisión humana lo hemos sacado en dos jornadas de trabajo intenso: PHP parcheado, las máquinas sin soporte apagadas, una web migrada, otra convertida en HTML, Node.js actualizado, HTTP/3 revisado, los equipos de desarrollo protegidos frente a eMudhra y los servidores al día.

No todo está cerrado. Quedan webs que migrar, un servicio que sacar de una versión sin soporte y unas cuantas tareas de limpieza que han salido de paso al revisarlo todo a fondo. Pero lo urgente está resuelto.

Lo que nos llevamos

  • Llegar preparados lo es todo. Tener la infraestructura inventariada, las copias disponibles y los permisos de los agentes bien delimitados es lo que nos ha permitido decidir y actuar rápido cuando han llegado los avisos. Uno detrás de otro.
  • La infraestructura, mejor en código. Si cada servidor está descrito en ficheros, cambiarlo es rápido, se puede revisar antes de aplicar y queda registrado. Y los agentes trabajan sobre algo que entienden.
  • Lo que crees que tienes no siempre es lo que tienes. Ha aparecido un servicio que no estaba en nuestra lista, y unas actualizaciones automáticas que estaban “activadas” llevaban tiempo sin actualizar nada. Hay que comprobar, no suponer.
  • Lo que no se usa, mejor quitarlo. Una web que nadie edita no necesita un WordPress detrás. Cada pieza que sobra es una pieza más que parchear.
  • Las versiones sin soporte son una bomba de relojería. Cuando llega un fallo grave, no hay parche. Mejor migrar con calma antes que con prisa después.

Si la respuesta a “¿tenéis vuestra infraestructura inventariada?” es “más o menos”, hoy es un buen día para empezar. Y si queréis ayuda, escribidnos.

Para los técnicos: cómo lo hicimos

A partir de aquí, los detalles: identificadores, versiones y lo que aprendimos por el camino.

Infraestructura como código

Todo lo que se cuenta a continuación se apoya en que la infraestructura está en repositorios git:

  • Trellis (Ansible) para los servidores WordPress: paquetes, versión de PHP, PHP-FPM, Nginx, fail2ban, firewall y actualizaciones automáticas. Cambiar el origen de PHP o activar bien las actualizaciones fue un cambio en el repositorio y un provision.
  • Dockerfiles y docker compose para las aplicaciones Node.js, con la imagen base declarada en el propio repositorio. Subir de Node fue cambiar una etiqueta o un digest y redesplegar.
  • Configuración de Caddy versionada junto a cada proyecto.

Cada cambio es un commit: se revisa antes de aplicarlo, se prueba primero en una VM local y en staging, y si algo falla se vuelve atrás. Los agentes leen esos repositorios para saber cómo está montado cada servidor y proponen los cambios como diffs que revisamos nosotros.

PHP: CVE-2026-109618 y CVE-2026-109619

Dos fallos de ejecución remota de código sin autenticar, con una puntuación CVSS 4.0 de 9,2.

  • CVE-2026-109618 (GHSA-w8hw-w9fx-j79w): corrupción de memoria cuando PHP se queda sin memoria con los datos de la petición a medio inicializar.
  • CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp): parte del estado de una petición sobrevive a la memoria liberada y la siguiente petición del mismo worker la reutiliza.

Corregidos en 8.5.12, 8.4.27, 8.3.36 y 8.2.35. Las ramas 7.4, 8.0 y 8.1 solo tienen backports en el código fuente. Detalle en el aviso oficial de php.net y, si usáis Trellis, en el hilo de Roots.

Cómo lo hicimos:

  • Mitigación de Roots. Roots publicó una mitigación provisional (session.upload_progress.enabled = Off). Los agentes revisaron nuestro código y ningún proyecto usaba esa función. La dejamos preparada, pero decidimos esperar a la versión oficial. El aviso de PHP confirmó después que la mitigación era real, aunque parcial. También proponía tocar memory_limit y post_max_size; no lo aplicamos porque podía romper subidas grandes.
  • Sury en lugar del PPA. Cuando salió la versión, el PPA de Ondřej, de donde instalaban PHP nuestros servidores, seguía en 8.4.26 y 8.3.35, pero packages.sury.org, del mismo mantenedor, ya tenía las corregidas. Cambiamos el origen, algo que Trellis ya tenía en main desde agosto. Ojo: el rol nuevo no borra el fichero del PPA si lo creó Ansible con otro nombre, y os quedan los dos repositorios a la vez.
  • Trellis instala PHP pero no lo actualiza. Con apt_package_state: present, nuestros servidores iban tres versiones por detrás. Para parchear hay que provisionar con apt_package_state=latest.
  • Cronología. Primer staging a las 15:58, después de validar en una VM local. Producción entre las 16:02 y las 16:16, un servidor cada vez, comprobando versión, origen de los paquetes, reinicio de PHP-FPM y que las webs devolvían los mismos códigos HTTP. El último staging, a las 16:49.
  • El bloqueo. Trellis limita las conexiones SSH y usa fail2ban. Tras tantas copias y comprobaciones, un servidor nos bloqueó más de media hora. Lo resolvimos con una conexión SSH persistente compartida entre Ansible y ssh, sin reintentos.
  • PHP 7.4. PHP publicó parches mínimos solo en código fuente y el repositorio de esas máquinas anunció que ese día no habría paquetes. Por eso las apagamos.
  • El riesgo es mayor con mod_php. Nosotros usamos PHP-FPM, pero eso no era excusa para quedarnos quietos.
  • Circuló un enlace a un supuesto “PHP 8.4.27 released” en el dominio php-net.pro, que no es php.net. Las fuentes buenas son php.net, los avisos de php-src en GitHub y los repositorios de paquetes de vuestra distribución.

Node.js: CVE-2026-111497 y compañía

La release de seguridad de octubre de Node.js corrige una escritura fuera de límites en el decodificador Brotli (CVE-2026-111497, gravedad alta) que puede acabar en ejecución remota de código, otros fallos altos de validación de hostname en TLS y dns.resolve, y varios menores según la rama.

Corregidos en 22.23.4, 24.22.1 y 26.12.1. La rama 20 está fuera de soporte y no recibe parche.

Cómo lo hicimos:

  • Leer el código antes de correr. El RCE de Brotli no entraba por las peticiones: Fastify no descomprime cuerpos sin @fastify/compress y Express 4 solo acepta gzip y deflate. Ojo con Express 5 (body-parser 2), que sí descomprime Brotli de entrada. La vía real era la contraria: fetch/undici descomprimiendo respuestas br de servicios externos.
  • Comprobar lo que corre de verdad. Con node -v dentro de cada contenedor. No se había actualizado ninguno, y apareció una herramienta de terceros que no estaba en la lista, con su propio Node dentro.
  • Cada proyecto, a su manera. Una imagen sin versión fija: reconstruir con --pull. Otra con la versión fija en el Dockerfile: cambiar la etiqueta y redesplegar. Y una con la imagen fijada por digest y producción congelada: hotfix desde el commit exacto que corría, cambiando solo los digests de Node y comprobando que el diff no llevaba nada más.
  • Tener la versión en el Dockerfile no basta. Un FROM sin fijar se queda con la imagen base en caché si no reconstruís con --pull.
  • Agentes en equipo. El agente del inventario coordinó y repartió el trabajo entre los agentes de cada proyecto, que conocen su forma de desplegar.
  • Pendiente. La herramienta de terceros aún no tiene versión con Node corregido, y un servicio sigue en la rama 20. A ese no le vale un parche: toca migrarlo a 24.

HTTP/3: amplificación QPACK

La investigación de FoxIO, bautizada como header accordions, muestra cómo con una sola conexión HTTP/3 sin autenticar un cliente obliga al servidor a reservar de decenas a miles de veces más memoria de la que envía. El problema está en QPACK, la compresión de cabeceras de HTTP/3 sobre QUIC.

  • CVE-2025-64702: quic-go, la librería que usan Caddy, Traefik y cloudflared. Corregido en 0.57.0.
  • CVE-2026-12523: Cloudflare quiche. Corregido en 0.29.3.
  • lsquic se corrigió en 4.9.0 sin CVE. Bun, la crate h3 y aioquic seguían sin parche al publicarse. IIS, Envoy, ngtcp2/nghttp3 y Google quiche no están afectados. nginx no aparece en la investigación.

Cómo lo hicimos:

  • Ver qué sirve HTTP/3 de verdad, es decir, qué escucha en UDP 443.
  • En Caddy, caddy build-info dice qué versión de quic-go lleva cada binario. Casi todos estaban ya corregidos.
  • Anunciar no es servir. Un Caddy llevaba un quic-go vulnerable y anunciaba HTTP/3 en la cabecera alt-svc, pero el contenedor no publicaba UDP 443 y el cortafuegos lo descartaba. Vulnerable sobre el papel, inalcanzable en la práctica. Aun así lo actualizamos a Caddy 2.11 y desactivamos el anuncio de HTTP/3, que no se usaba.

Python: CA de eMudhra comprometida

Las ocho CA intermedias de eMudhra/emSign tenían claves privadas con d < 256, así que cualquiera podía emitir certificados válidos para cualquier dominio, pypi.org incluido. La raíz emSign sale de certifi. Detalle en el aviso de la Python Software Foundation.

La trampa: pip anterior a 26.2.2 sigue confiando en su certifi vendorizado aunque el sistema ya esté parcheado, así que actualizar el sistema o el certifi externo no basta. Mínimos: pip 26.2.2, uv 0.13.4, certifi 2026.10.18 (recomendada 2026.10.19), pixi 0.82.1 y sigstore 4.5.1.

Cómo lo hicimos:

  • Un prompt para revisar cada máquina. Lo hemos publicado en un gist. Primero hace inventario sin tocar nada: intérpretes, venvs, ensurepip, pipx, uv, conda, overrides de TLS, Docker, CI y lockfiles. Después, con aprobación, actualiza en orden seguro: pip desde el wheel con su SHA256 verificado e instalado con --no-index, y luego pipx, uv y certifi.
  • Segunda opinión. Pasamos el prompt por otro modelo, GPT-6 Astra con Codex, que confirmó versiones, URL y hash del wheel y propuso correcciones que ya están incorporadas.
  • Ojo con los .pyc. En un Python, pip seguía diciendo 26.2.1 después de instalar el wheel nuevo: los .pyc antiguos tenían el mismo tamaño y fecha que los .py nuevos y Python los daba por buenos. Se arregló borrando los __pycache__ de pip. Comprobad pip.__version__, el código que se carga, y no solo el dist-info.
  • Los .trellis/virtualenv también cuentan. Cada proyecto Trellis tiene su propio virtualenv para Ansible, con su propio pip.
  • Pendiente. Algunos ensurepip antiguos siguen trayendo pip vulnerable, así que un venv nuevo creado con ellos nace sin parchear. Y Node trae su propio almacén de certificados, que se corrige al actualizar Node.

Actualizaciones automáticas que no actualizan

  • Las actualizaciones automáticas de seguridad estaban “activadas” en algunos servidores, pero les faltaba la directiva que las aplica. Había más de cien actualizaciones de seguridad acumuladas por máquina.
  • Las aplicamos con un playbook de Trellis, probado antes en una VM local, sin reinicio y sin cortes, y dejamos bien configuradas las automáticas. Los reinicios por el kernel van en horario de poco tráfico, y una subida de versión mayor de MariaDB la hemos separado a propósito.
  • Para que los deploys no fallaran al clonar el repositorio, la conexión SSH persistente necesitaba también reenvío del agente.

Y hasta aquí la parte técnica. Si queréis que revisemos vuestra web o vuestra infraestructura antes de que llegue el próximo aviso, escribidnos y lo vemos juntos.