Saltar al contenido

El 22 de mayo de 2026, sobre las 22:32 UTC, un atacante con acceso de escritura a la organización Laravel-Lang en GitHub reescribió todos los tags de versión de cuatro paquetes de localización muy usados. No subió ningún commit malicioso a los repositorios oficiales. Reescribió etiquetas que ya existían para que apuntaran a un commit envenenado alojado en un fork bajo su control. En unos quince minutos, cualquiera que ejecutara composer update o una instalación nueva se llevaba a casa un ladrón de credenciales.

Los paquetes afectados fueron laravel-lang/lang (con 7.8k estrellas en GitHub), laravel-lang/attributes, laravel-lang/http-statuses y laravel-lang/actions. Ninguno forma parte del framework oficial de Laravel: son librerías de la comunidad para traducciones e internacionalización, pero están presentes en una enorme cantidad de starter kits, paneles de administración y boilerplates de SaaS. Aikido contabilizó 233 versiones comprometidas; Socket elevó la cifra a unas 700 al contar todos los tags históricos reescritos.

Este artículo es un análisis de lo que pasó, por qué pudo pasar, y qué conclusión práctica deberíamos sacar todos los que vivimos de escribir código.

Qué tipo de ataque fue

Esto es un ataque de cadena de suministro (supply chain attack), y conviene entender por qué es más peligroso que un SQLi o un XSS clásico. En un ataque tradicional, alguien ataca tu aplicación. En un ataque de cadena de suministro, alguien ataca algo en lo que tu aplicación confía, y entra contigo de la mano sin que tu código tenga ningún fallo.

El vector concreto aquí es lo que se conoce como git tag rewrite. Funciona así:

  1. El atacante consiguió acceso de escritura a la organización Laravel-Lang. La hipótesis más sólida es un Personal Access Token (PAT) filtrado, presumiblemente de una brecha reciente de GitHub.

  2. Creó un fork de cada repositorio y metió el código malicioso ahí, en su propio fork.

  3. Reescribió los tags de versión de los repositorios oficiales para que apuntaran a los commits de su fork. GitHub permite que un tag de un repositorio apunte a un commit que vive en un fork de ese mismo repositorio.

  4. Packagist resolvió esos tags y empezó a servir el código malicioso bajo números de versión legítimos y de toda la vida.

La consecuencia es la parte verdaderamente desagradable: no había forma de detectarlo mirando el repositorio oficial. La vista del código en GitHub seguía estando limpia. Las monitorizaciones tradicionales vigilan merges y commits a ramas, no la deriva silenciosa de un tag. Por eso varios analistas calificaron la técnica de innovadora.

Qué hacía el malware

El payload llegaba en dos fases.

Fase 1, el dropper. El atacante introdujo un fichero src/helpers.php que, a simple vista, parecía un helper rutinario de localización con dos funciones inocentes. El detalle crítico está en que ese fichero quedaba registrado en composer.json bajo la directiva autoload.files. Eso significa que se ejecutaba automáticamente en cada request PHP de la aplicación infectada, sin necesidad de que nadie llamara a nada. Bajo las funciones inocentes había un bloque autoejecutable que tomaba huella del host, escribía un fichero marcador en el directorio temporal para no dispararse dos veces, y descargaba el payload real de un dominio C2 (flipboxstudio.info) ofuscado dentro de un array de enteros para esquivar los escáneres estáticos.

Fase 2, el ladrón. El payload descargado era un robador de credenciales de unas 5.900 líneas de PHP, organizado en quince módulos especializados. Cifraba todo lo que encontraba con AES-256, lo mandaba al C2, y luego se borraba a sí mismo del disco para dejar el mínimo rastro forense. ¿Qué se llevaba? Prácticamente todo lo que tuvieras a mano:

  • Credenciales cloud: claves de AWS (incluida la metadata de instancias EC2 en vivo), GCP, Azure, DigitalOcean, Heroku, Vercel, Netlify, Railway, Fly.io.

  • Secretos de infraestructura: kubeconfigs (incluido /etc/kubernetes/admin.conf), tokens de HashiCorp Vault, configs de Docker.

  • Credenciales de desarrollo: claves SSH privadas, .git-credentials, .npmrc, .composer/auth.json, historiales de shell, y todos los ficheros .env y de configuración que encontrara escaneando recursivamente el directorio de trabajo.

  • Contraseñas guardadas de 17 navegadores basados en Chromium, además de Firefox, KeePass, 1Password y Bitwarden.

  • Carteras de criptomonedas, tokens de Slack, Discord y Telegram, y configuraciones de VPN.

En resumen: si te tocó, asume que todo lo que tu entorno podía tocar quedó comprometido. La recomendación de los investigadores fue rotar todos los secretos alcanzables desde cualquier entorno donde se instalara una versión envenenada.

Por qué Composer no lo detuvo

Aquí está la lección de arquitectura, y no es un fallo puntual, sino una propiedad del diseño.

Composer resuelve una restricción de versión (^3.4, ~2.0, etc.) buscando el último tag que la cumple en el repositorio upstream y descargando el commit al que ese tag apunta en ese momento. No verifica criptográficamente el commit. No compara contra un hash que hubiera visto previamente para ese mismo tag. Y los tags de Git son referencias mutables: cualquiera con acceso de escritura puede recrear un tag apuntando a otro commit.

Esto deja al descubierto una limitación importante: el pinning de versión no protege contra esto. Fijar laravel-lang/lang: 13.15.0 no sirve de nada si el tag 13.15.0 se reescribe en el sitio. Lo único que estaba a salvo eran los proyectos cuyo composer.lock fijaba un commit SHA anterior al ataque y que solo ejecutaban composer install contra ese lock, nunca update ni require. En cuanto ejecutas un update, Composer refresca la metadata del registro y reescribe las entradas del lock, aunque la versión no cambie.

Qué está haciendo la comunidad PHP

La respuesta de Packagist y Composer ha sido rápida y, conviene decirlo, en buena parte ya estaba en marcha antes del incidente. Esto es lo verificado:

Ya en producción:

  • Desde marzo de 2026, Packagist.org importa los resultados de detección de malware de Aikido. Cuando una versión se marca, el aviso aparece en la UI y se incluye en la metadata que consume Composer. Aikido marcó las versiones maliciosas en cada uno de los incidentes recientes.

  • Un transparency log público que registró con precisión cada una de las reescrituras de tags, lo que permitió reconstruir la cronología exacta del ataque.

Recién lanzado (semana del 27 de mayo de 2026):

  • Composer 2.10, que introduce un framework unificado de dependency policies cubriendo versiones marcadas como malware, advisories de vulnerabilidades y paquetes abandonados. El bloqueo de malware viene activado de serie. Este es, de hecho, el motivo por el que muchos despliegues empezaron a fallar de golpe: Composer se niega a instalar una versión marcada.

  • Inmutabilidad de versiones estables en Packagist.org: las versiones publicadas ya no se pueden reescribir silenciosamente reetiquetando en Git. Los cambios de tag upstream se detectan y se rechazan, y se notifica al mantenedor. Esto ataca la raíz exacta del problema.

En camino:

  • La política de minimum release age (periodo de enfriamiento): rechazar instalar una versión publicada hace menos de N horas. Es una de las defensas más efectivas contra esta clase de ataque, porque las versiones maliciosas suelen detectarse y retirarse en cuestión de horas. Esta política copia un enfoque que npm, pnpm, Yarn y Bun ya tienen, y está discutida en el issue #12877 de Composer.

  • MFA obligatorio progresivo, estado de MFA visible en los perfiles de mantenedor, un flujo de publicación por fases con confirmación FIDO2 para paquetes muy usados, y a más largo plazo artefactos inmutables con provenance SLSA y atestaciones Sigstore.

No es terreno nuevo: PyPI hizo el 2FA obligatorio en enero de 2024 y npm lleva desde 2023 con provenance firmada vía Sigstore. PHP estaba por detrás en algunas de estas piezas, sobre todo en MFA obligatorio, y este incidente ha acelerado el cierre de esos huecos.

¿Tienes uno de estos paquetes instalado? Cómo saber si estás afectado

Si en tu composer.json aparece laravel-lang/lang, laravel-lang/attributes, laravel-lang/http-statuses o laravel-lang/actions, esto es lo que tienes que hacer, en orden.

1. Para inmediatamente los update. No ejecutes composer update ni composer install sin un lockfile que sepas bueno en ningún proyecto que dependa de estos paquetes. Cada update puede resolver a un tag envenenado.

2. Determina si estás a salvo o comprometido. La pregunta clave es: ¿cuándo se generó por última vez tu composer.lock?

  • Si tu composer.lock se generó antes del 22 de mayo de 2026 a las 22:32 UTC y solo has corrido composer install contra él desde entonces, estás a salvo. El lock fija un commit SHA concreto, no un tag.

  • Si regeneraste el lock (un update, require, o un build de CI) en esa fecha o después, trátalo como comprometido hasta demostrar lo contrario.

Comprueba la fecha del lock y si los paquetes están presentes:

# Fecha de la última modificación del lockfile
git log -1 --format=%ci -- composer.lock

# ¿Están los paquetes afectados en el lock?
grep -E "laravel-lang/(lang|attributes|http-statuses|actions)" composer.lock

3. Verifica el commit SHA fijado. Abre tu composer.lock, localiza el bloque de cada paquete y mira el campo source.reference (el SHA). Compáralo con los commits impostores publicados por los investigadores. Si tu SHA coincide con uno de ellos, estás infectado. Algunos de los commits maliciosos confirmados:

# laravel-lang/http-statuses (commits maliciosos confirmados)
#   v3.4.5 -> bba2e443dc7ff1f8704f52a5375383e3f4f643b8
#   v3.4.0 -> 26c233e1a0d4fd2331e8e0f175e18f8eed904aa3
#   v1.0.0 -> 6b1d5782a8c8c199d070857802d39bfe609eb6f2
#
# laravel-lang/actions
#   1.12.2 -> 556d2b335d4d6d92139822017ee461b668afe375
#
# Señal universal en TODOS los repos afectados:
#   el commit del tag modifica solo composer.json y src/helpers.php,
#   con autor "Your Name <[email protected]>"

4. Busca rastros de ejecución. El payload se autoborra del disco en unos 3 segundos, así que en una máquina que lleva días encendida probablemente no encuentres los ficheros. Pero los procesos siguen vivos en memoria, reparentados a init (ppid=1):

# Procesos huérfanos sospechosos (php o binario ELF sin nombre, ppid 1)
ps -eo pid,ppid,comm,args | awk '$2==1 && /php|tmp/'

# Artefactos en /tmp (si revisas justo después de un install infectado)
ls -la /tmp/.laravel_locale/ 2>/dev/null

5. Bloquea el dominio C2. Mete flipboxstudio.info en tu blocklist de egress, firewall o DNS sinkhole. Es un typosquat del legítimo flipboxstudio.com. Cualquier conexión saliente a ese dominio confirma compromiso.

6. Si estuviste expuesto, rota TODO. Si corriste un install de estos paquetes el 22/05 a las 22:32 UTC o después, asume que cualquier secreto alcanzable desde ese entorno se ha filtrado y rótalo: tokens de CI, credenciales de AWS/GCP/Azure, GITHUB_TOKEN y cualquier PAT, claves de despliegue, credenciales de base de datos, ficheros .env, claves SSH. No es paranoia: el stealer iba a por exactamente eso.

7. La salida limpia. No hay versión segura a la que actualizar, porque todos los tags se reescribieron. Las opciones son: fijar un commit SHA verificado anterior al ataque, o directamente quitar la dependencia. Si Composer 2.10 te está bloqueando el deploy por marca de malware, ese bloqueo es el sistema funcionando como debe; no lo desactives con policy.malware.block=false para "salir del paso". Resuelve la dependencia primero.

Lo que de verdad se lleva uno de aquí

Este ataque no entró por un fallo en tu código, ni por una librería oscura de cuatro descargas al mes. Entró por paquetes con miles de estrellas, presentes en proyectos de medio mundo. La popularidad no es sinónimo de seguridad. Es justo lo contrario: los atacantes priorizan sus objetivos por número de descargas y por el grafo de dependencias, ambos datos públicos. Cuanto más usado es un paquete, más interesante es comprometerlo.

Y cada dependencia que añades es código de terceros que se ejecuta con los mismos permisos que el tuyo, a veces literalmente en cada request vía autoload.files, como demostró este caso, y que confía en una cadena de credenciales, mantenedores y registros que tú no controlas. Cada composer require es una decisión de confianza, no un detalle de implementación.

Por eso el lema sigue siendo el mismo, y este incidente lo confirma: menos dependencias, más tranquilidad. No es purismo ni "reinventar la rueda". Es reducir superficie de ataque. Antes de instalar algo, vale la pena preguntarse si lo necesitas de verdad, si podrías resolverlo con unas pocas líneas propias, y si el coste de mantenerlo tú mismo es mayor que el riesgo de delegar en una cadena que cualquier noche de mayo puede reescribirse entera en quince minutos.

Las medidas de Packagist y Composer van en la dirección correcta y elevan mucho el coste de estos ataques. Pero la primera línea de defensa siempre la decides tú, en el momento de teclear composer require.


Fuentes verificadas: Aikido Security, StepSecurity, Snyk, blog oficial de Packagist (27 de mayo de 2026) e issue #12877 del repositorio de Composer.

school Curso completo

Curso Laravel 12
Completo 2026

El único curso 100% actualizado que incluye Laravel 12, Livewire 3, Vue 3, React 19 e Inertia 2. Aprende con proyectos reales y las últimas funcionalidades.

access_time 8 horas de contenido
layers 4 tecnologías en 1
update 100% actualizado
code Proyectos prácticos
Ver Curso Laravel 12 arrow_forward

star Incluido en cualquier suscripción

Rutas de aprendizaje