Principales conclusiones
- Packagist indexa las etiquetas de Git en lugar de alojar los artefactos, por lo que, hasta julio de 2026, una versión publicada podría reorientarse discretamente hacia un commit diferente.
- En dos incidentes ocurridos en 2026 se utilizó esa misma técnica: en uno se registró un complemento malicioso de Composer y en el otro se secuestró el autocargador de PHP.
- Un comprometido
composer.lockasigna la confirmación que ya se había resuelto, por lo que una reescritura posterior de la etiqueta no puede modificar lo quecomposer installtirones. - Ahora, Packagist rechaza las modificaciones de etiquetas de las versiones estables publicadas, pero aún hay que revisar los archivos de bloqueo antiguos y las ramas de desarrollo.
El resto de registros importantes tomaron la misma decisión desde el principio. Una vez que se lanza una versión, su contenido queda fijado. Los archivos tar de npm son inmutables. PyPI no permite volver a subir un archivo con el mismo nombre. crates.io permite retirarlo, pero no eliminarlo. Maven Central no elimina en absoluto un artefacto publicado.
Packagist tomó una decisión diferente. No almacena paquetes. Lee un repositorio de Git, indexa las etiquetas que encuentra e indica a Composer de dónde debe obtenerlas. Una versión como la 2.4.1 es una etiqueta de Git más la referencia de commit que Packagist registró para ella, y hasta julio de 2026 esa etiqueta podría eliminarse y volver a enviarse asociada a un commit diferente, sin que Packagist se diera cuenta.
Esa vulnerabilidad es la que aprovecharon los ataques a la cadena de suministro de Composer de 2026, y PHP sufrió dos casos concretos con tres semanas de diferencia.
Por qué Packagist es un índice y no un registro
La diferencia arquitectónica se traduce en una diferencia en materia de seguridad. Cuando Composer resuelve una dependencia, Packagist proporciona metadatos: el nombre del paquete, sus versiones y la ubicación de cada versión, incluidas las referencias de código fuente y de distribución, que suelen ser un hash de commit. El código real procede del repositorio de código fuente o de un archivo de distribución que el proveedor de código genera bajo demanda a partir de una referencia específica. Ningún elemento de esa cadena es un artefacto inmutable que almacene el propio Packagist.
| Registro | Identidad de la versión | ¿Puede cambiar una versión ya publicada? |
|---|---|---|
| npm | Un archivo tar publicado | No, las versiones son inmutables |
| PyPI | Un archivo subido | No, las subidas no se pueden sustituir |
| crates.io | Un archivo publicado | No, solo para estadounidenses |
| Maven Central | Un artefacto publicado | No, no está permitido borrarlo |
| Packagist | Una etiqueta de Git más una referencia a una confirmación indexada | No desde julio de 2026; antes de esa fecha se seguían las reetiquetaciones. |
Esa última fila resume a la perfección el riesgo de la cadena de suministro de PHP, y no se trata de un error que Packagist haya introducido por accidente. La indexación de Git mantiene los obstáculos a la publicación en niveles inusualmente bajos, y eso ha sido durante mucho tiempo una de las ventajas prácticas de Packagist.
Análisis realista: Calificar esto como una vulnerabilidad de Packagist pasa por alto dónde reside realmente la mutabilidad. Las etiquetas de Git siempre han sido movibles por diseño. Packagist heredó esa propiedad al optar por indexar repositorios en lugar de alojar artefactos, y esa misma compensación es lo que hace que publicar un paquete de Composer resulte relativamente sencillo.
Cómo es realmente un ataque a la cadena de suministro de un compositor
El 30 de abril de 2026, alguien que tenía acceso a una cuenta de servicio comprometida publicó una modificación maliciosa en el repositorio «intercom/intercom-php» y le asignó la etiqueta existente 5.0.2, según el Aviso de GitHub. La versión reescrita composer.json declaró el paquete como un complemento de Composer y, una vez aprobado —ya fuera mediante una solicitud interactiva o una configuración permisiva— allow-plugins escenario: se representó durante composer install como un código malicioso que obtenía el entorno de ejecución de Bun y ejecutaba un programa ofuscado para robar credenciales. El código malicioso estuvo activo menos de dos horas antes de que los responsables del mantenimiento lo sustituyeran por código limpio. Los investigadores lo bautizaron como «Mini Shai-Hulud» y lo relacionaron con la campaña que afectó a El cliente npm de Intercom en la misma ventana.
Tres semanas más tarde, esa misma vulnerabilidad se aprovechó a una escala mucho mayor. A partir de las 22:32 UTC del 22 de mayo y hasta bien entrado el 23 de mayo, un atacante con acceso de escritura en toda la organización reescribió todas las etiquetas existentes en cuatro paquetes de localización de Laravel-Lang, redirigiéndolas hacia commits de una bifurcación que controlaba, de modo que nunca se introdujo nada malicioso en los repositorios oficiales. Socket contabilizó más de 700 versiones históricas reasignadas, Aikido confirmó 233, y la oleada principal de reescritura duró menos de 90 minutos. En esta ocasión, la carga útil secuestró el autocargador de PHP en lugar de registrar un complemento.
Ninguno de los dos incidentes requirió la publicación de una nueva versión. Ni una entrada en el registro de cambios, ni un nuevo número de versión, ni nada que un responsable del mantenimiento o un bot de dependencias que supervise los números de versión pudiera señalar como diferente.
Compáralo con cómo tiene que actuar un atacante en npm. Sustituir una versión legítima allí implica publicar un número de versión diferente, ya que los bytes que hay detrás de un paquete y una versión existentes no se pueden cambiar a tu espalda. Una nueva versión aparece en los canales de publicaciones y activa los bots de dependencias. Mover una etiqueta no provocaba nada de eso.
| Fase de ataque | npm | Packagist antes de julio de 2026 |
|---|---|---|
| Publicar código malicioso | Se requiere un nuevo número de versión | La versión actual podría volver a indicarse |
| Acto de presentación de un nuevo lanzamiento | Sí | No |
| Detectado por herramientas de comparación de versiones | Sí | Aunque a menudo pasa desapercibido, el número de versión nunca cambió |
| Revocado por el desarrollador original | Se aplica la ventana «Retirar de la publicación» | La etiqueta se podría volver a colocar en su sitio sin más |
La reversión tenía sus pros y sus contras. La recuperación fue rápida —un responsable de mantenimiento solo tenía que volver a mover la etiqueta, que es exactamente lo que ocurrió con intercom-php esa misma noche—, pero el análisis forense se complicó por la misma razón, ya que un número de versión podía apuntar a una confirmación diferente a la habitual. El registro público de transparencia de Packagist registró esos cambios en las referencias, lo que al menos deja un registro duradero de cuándo se movió una referencia publicada.
Dos cargas útiles diferentes, una etiqueta modificable
Los dos incidentes de 2026 fueron ataques a la cadena de suministro de Composer en el sentido más estricto, y ambos comenzaron con el mismo truco. Lo que hicieron a continuación fue completamente diferente, y esa distinción es muy importante a la hora de decidir qué hay que solucionar realmente.
La etiqueta comprometida en intercom/intercom-php apuntaba a una confirmación que modificaba composer.json para declarar el paquete como un complemento de Composer. Esa es la variante de este ataque a la que recurren la mayoría de los análisis, ya que encaja perfectamente en el sistema de complementos de Composer. Un complemento autorizado se ejecuta durante composer install y composer update, antes de que se ejecute cualquier parte del código de tu aplicación.
La vulnerabilidad de Laravel-Lang nunca afectó al registro de complementos. Lo que sí hizo fue conectar un código malicioso src/helpers.php hacia el autoload.files entrada, por lo que la carga útil se ejecutaba en cada solicitud PHP desde el momento en que se instalaba el paquete. La misma etiqueta «moved», la misma confianza que Composer deposita en aquello a lo que resuelve la etiqueta, dos rutas de ejecución completamente diferentes. Una solución para una de ellas no sirve de nada para la otra.
También puedes conocer las principales estrategias de protección del framework en nuestra guía detallada sobre seguridad en Laravel.
La seguridad del complemento Composer tiene una verdadera brecha
Una etiqueta desplazada ejecuta código. Algo tiene que ejecutarla y, en el caso del ataque de tipo «intercom», ese algo es el sistema de complementos. Un paquete que declare "type": "composer-plugin" Se carga y puede ejecutar código como parte de los propios procesos de instalación y actualización de Composer, una vez que se le permite hacerlo. Ese es el comportamiento previsto, ya que los complementos sirven para modificar la forma en que Composer instala los componentes, lo que implica que se ejecutan antes de que finalice la instalación.
En la versión 2.2.0 de Composer se añadió la allow-plugins config, y la configuración predeterminada actual no permite nada en absoluto. En composer.json Tiene este aspecto.
{
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"phpstan/extension-installer": true
}
}
}
Con allow-plugins Una vez configurado, Composer se niega a ejecutar cualquier complemento que no figure en la lista. Si se ejecuta Composer de forma interactiva, aparece un mensaje de aviso cuando se intenta ejecutar un complemento que no está en la lista; y, en una ejecución no interactiva, Composer falla en lugar de continuar sin avisar, a menos que el complemento se declare opcional. Es mejor que falle la compilación a que la instalación se realice en silencio.
Cuidado: Comprueba si alguien ha configurado "allow-plugins": true solo para que desaparezca un mensaje. Esa única línea incluye en la lista de permitidos todos los complementos, tanto actuales como futuros, y desactiva por completo la protección de la lista de permitidos. Al ejecutar composer config allow-plugins muestra la configuración real de un proyecto, y false es el valor que lo niega todo.
Para una compilación que no requiere ningún complemento, las opciones son sencillas y eficaces.
composer install --no-plugins --no-scripts --no-dev
Si tu entorno de producción no necesita plugins ni scripts de Composer, una solución razonable es desactivarlos allí, pero dejarlos disponibles en los equipos de los desarrolladores.
Los archivos de carga automática son la opción más discreta
allow-plugins No sirve de nada frente a un ataque del tipo Laravel-Lang. autoload.files No es una función opcional que se pueda restringir mediante una solicitud de permiso. Es una forma habitual en la que los paquetes de PHP incluyen funciones auxiliares, y Composer tiene que confiar en la configuración de autocarga de cada paquete para que el ecosistema funcione en absoluto.
Eso es lo que hizo que esta versión del ataque tuviera un impacto tan fuerte. Composer te pregunta si confías en un paquete para que se ejecute como complemento. Nunca te pregunta si confías en un paquete para que defina archivos que se cargan automáticamente. Antes de la política contra el malware de Composer 2.10 y del cambio de inmutabilidad de Packagist, la principal protección en este caso consistía en no resolver nunca la etiqueta reescrita, que es lo que hacía que el archivo de bloqueo fuera tan importante.
Lo único en lo que Composer acierta y npm no
Los scripts de Composer declarados por una dependencia no se ejecutan. Solo se ejecutan los scripts del paquete raíz. Un script malicioso post-install-cmd Situarlo cuatro niveles más abajo en el árbol no sirve para nada. Es una configuración por defecto significativamente mejor que la de npm, donde cualquier paquete del árbol puede incluir un script de postinstalación que se ejecute durante la instalación.
| Mecanismo | Ejecuciones para las dependencias |
|---|---|
| Scripts del ciclo de vida de npm | Sí, cualquier paquete del árbol |
Python .pth ganchos de inicio |
Sí, al iniciar el intérprete |
Óxido build.rs |
Sí, durante la compilación |
| Scripts de Composer | No, solo el paquete raíz |
| Complementos de Composer | Sí, si está permitido |
¿Sabías que...? Ignorar los scripts de dependencias es una decisión de diseño deliberada de Composer, no una medida de seguridad añadida a posteriori tras un incidente. Es uno de los detalles que más se pasan por alto a la hora de comparar ambos ecosistemas.
Lo que registra el archivo de bloqueo de tu reproductor y lo que no registra
composer.lock ofrece más protección de lo que sugiere su reputación, y dicha protección tiene un límite concreto, que casualmente abarca tanto el ataque de complementos como el de autocarga, ya que opera un nivel por debajo de ambos.
Para cada paquete, el archivo de bloqueo registra la versión resuelta junto con las referencias de origen y distribución, normalmente el hash de la confirmación de Git correspondiente a esa versión. Ejecuta composer install y Composer solicita precisamente esa confirmación concreta, en lugar de lo que la etiqueta señale en la actualidad, por lo que una reescritura posterior de la etiqueta nunca entra en juego. La garantía se refiere a qué confirmación se solicita, no a una suma de comprobación de los bytes que se devuelven, ya que los servidores de código generan esos archivos bajo demanda y el archivo de bloqueo no contiene ningún hash útil para ellos. Las operaciones que vuelven a resolver las dependencias eran el punto de vulnerabilidad, y eso incluye actualizaciones parciales como composer update foo -W. El artículo sobre seguridad de The Packagist plantea lo mismo con respecto a los archivos de bloqueo, algo que conviene saber, ya que gran parte de la información publicada en Laravel-Lang afirmaba que los archivos de bloqueo no ofrecían protección alguna.
| Funcionamiento | Mantiene la referencia bloqueada | Expuesto a una etiqueta desplazada |
|---|---|---|
composer install con el archivo de bloqueo confirmado |
Sí | No |
composer update |
No, se vuelve a resolver | Sí |
| Actualización parcial de un paquete | Solo para las entradas que no modifica | Sí, para cualquier cosa que se vuelva a resolver |
composer require newpackage |
En parte | Sí, para el nuevo requisito y cualquier dependencia que este implique. |
| Clon nuevo con el archivo de bloqueo en la lista de ignorados de Git | No | Sí |
composer install en una compilación de Docker sin que se haya copiado el archivo de bloqueo |
No | Sí |
Un archivo Dockerfile que solo copia composer.json antes de ejecutar la instalación, no hay ningún archivo de bloqueo a partir del cual trabajar, y la protección desaparece sin que nadie haya modificado ni una sola línea del código de la aplicación. Al copiar composer.lock Además, devuelve la protección.
git ls-files composer.lock
Si eso no devuelve ningún resultado, composer.lock no se controla en este repositorio, por lo que cada compilación limpia resuelve las dependencias de nuevo según lo que indiquen los metadatos del registro en ese momento.
Detección de malware y el cambio de inmutabilidad de julio
Los incidentes de mayo no quedaron sin respuesta. Nils Adermann e Igor Benko publicaron una hoja de ruta de seguridad para Composer y Packagist el 27 de mayo, y al día siguiente se lanzó Composer 2.10.
Hay un aspecto de esa versión que nos concierne directamente. Packagist ya llevaba importando los resultados de detección de malware de Aikido desde marzo, y la versión 2.10 fue la que permitió que eso resultara útil en la práctica. Las versiones marcadas se eliminan del conjunto de resoluciones, por lo que composer update, composer require y composer create-project no puedo contactar con ellos, y composer install da un error evidente si ya hay una versión marcada en tu archivo de bloqueo, en lugar de incorporarla de forma silenciosa. composer audit muestra las mismas versiones y falla por defecto.
En junio, las organizaciones de Private Packagist también obtuvieron controles de restricción de complementos, lo que permite a una organización decidir de antemano qué paquetes pueden actuar como complementos de Composer. Con esta opción activada, una versión maliciosa que de repente se declare como complemento queda excluida de los metadatos de actualización y nunca llega al resolutor de Composer, aunque las instalaciones a partir de un archivo de bloqueo existente también requieren la configuración independiente de bloqueo de descargas de Private Packagist.
La solución estructural más importante se puso en marcha el 7 de julio, cuando Packagist lanzó las versiones inmutables. Una vez publicada una versión estable, la referencia de Git subyacente ya no cambia. Aunque se elimine, se vuelva a crear o se mueva la etiqueta en el repositorio original, Packagist conserva la referencia original, muestra un indicador de «bloqueo de reetiquetado» en la página del paquete y envía un correo electrónico a todos los responsables del mantenimiento.
Las versiones de desarrollo que siguen ramas siguen moviéndose, ya que ese es el objetivo de una rama, y la norma se refiere a la correspondencia entre la versión y la confirmación, más que a los bytes del archivo, porque la mayoría de los archivos ZIP siguen generándose bajo demanda por el servidor de código. Los cambios en referencias bloqueadas, las eliminaciones temporales y las recuperaciones quedan registrados en el registro público de transparencia.
Nada de eso borra con carácter retroactivo un archivo de bloqueo que haya capturado una confirmación maliciosa antes de que se implementaran las medidas de protección, y precisamente por eso composer.lock y allow-plugins Aún así, debes comprobar tus propios proyectos, independientemente de lo que indique el escáner.
Comprobación de los paquetes antes de que se resuelva una etiqueta
En ambos ataques, la referencia peligrosa entró en el gráfico de dependencias en el momento de la resolución, mucho antes de que se cargara un complemento o se activara un secuestro del autocargador. Composer 2.10 añade ahora también una comprobación de malware en el momento de la instalación, aunque dicha comprobación sigue dependiendo de que alguien haya marcado previamente la versión como sospechosa.
SecureChain para PHP ofrece a los equipos un repositorio de paquetes reconstruidos a partir del código fuente y sometidos a análisis de malware, en lugar de recurrir directamente a las referencias del sistema de control de versiones (VCS) de origen. Cada paquete se recompila en un entorno aislado y se somete a un proceso de verificación antes de que esté disponible para su instalación, por lo que lo que se descarga es exactamente lo que se ha verificado. Las vulnerabilidades se corrigen continuamente en virtud del SLA, y el soporte de ciclo de vida ilimitado garantiza esa cobertura incluso cuando el responsable del mantenimiento deja de publicar actualizaciones. Esto es especialmente importante en el caso de los paquetes abandonados, en los que ya no existe la supervisión del responsable del mantenimiento que detectaría un cambio inesperado en el código original.
Nada de eso sustituye a composer.lock, allow-plugins o composer audit. Resuelve la parte del problema que se encuentra antes de lo que pueden detectar tus propias herramientas. Mira cómo SecureChain para PHP funciona.
PREGUNTAS FRECUENTES
¿Qué es un ataque a la cadena de suministro de Composer?
Un ataque que compromete un paquete PHP antes incluso de que llegue a tu proyecto, normalmente a través de un repositorio comprometido, una etiqueta de Git modificada o un complemento malicioso de Composer, y no a través de una vulnerabilidad en el código que tú mismo has escrito. Se trata de una versión específica del patrón más amplio de ataques a la cadena de suministro que ha estado afectando a los registros de código abierto en todos los lenguajes.
¿Puede cambiar la versión de un paquete de Packagist tras su publicación?
Históricamente, sí era posible. Packagist indexaba las etiquetas de Git en lugar de almacenar artefactos inmutables, por lo que se podía eliminar una etiqueta y volver a enviarla asociada a un commit diferente sin que cambiara el número de versión. Desde julio de 2026, Packagist rechaza cualquier intento de modificar la referencia de origen de una versión estable ya publicada, una protección del lado del servidor que se aplica independientemente de la versión de Composer que utilice el desarrollador. Las versiones de desarrollo que siguen ramas siguen funcionando, y el cambio no puede garantizar la seguridad de las instalaciones históricas si estas resolvieron anteriormente una confirmación maliciosa.
¿Se ejecutan los scripts de dependencias de Composer durante la instalación?
No. Solo se ejecutan los scripts del paquete raíz. Un archivo malicioso post-install-cmd Se ignora la declaración de una dependencia, lo que supone un valor por defecto más estricto que el de npm.
¿Cómo ejecutan código los complementos de Composer?
Un paquete con "type": "composer-plugin" puede ejecutar código como parte de los procesos de instalación y actualización propios de Composer, una vez que se haya instalado y se le haya dado permiso. Composer 2.2.0 y versiones posteriores requieren que los complementos se incluyan explícitamente en la lista de permitidos a través del allow-plugins config.
¿Protege el archivo «composer.lock» contra el cambio de ubicación de una etiqueta de Git?
Sí, si el archivo de bloqueo ya contiene la confirmación válida, porque composer install utiliza esa referencia bloqueada en lugar de volver a resolver la etiqueta. No protege composer update, nuevo composer require llamadas o compilaciones en las que no existe el archivo de bloqueo.
¿Qué efecto tiene establecer «allow-plugins» en «true»?
Incluye en la lista de permitidos todos los complementos de Composer, tanto actuales como futuros, lo que elimina por completo la protección. Sustitúyelo por una lista explícita de los complementos específicos que necesita tu proyecto.



