Una API de Express llamada api-service puede superar las pruebas, generar una imagen OCI y, aun así, incluir código que nadie pretendía aprobar. Un desarrollador añade expresss en lugar de express al archivo package.json; el registro público resuelve el paquete con el nombre similar, el ejecutor de GitHub Actions lo instala y la imagen de lanzamiento lo contiene. El atacante no necesitó acceso de escritura al repositorio, ni una clave de despliegue robada, ni acceso al clúster de producción. La pregunta que debe plantearse quien defiende el sistema es más concreta y útil: ¿cómo evitamos que código no revisado se incorpore a una compilación mediante la resolución de dependencias? Seguiremos el ciclo de api-service desde su manifiesto, pasando por su compilación de CI, hasta llegar a la imagen publicada.
[fuentes][/fuentes]
El proceso de compilación atraviesa varios límites de confianza antes de que la imagen llegue a la fase de despliegue. Un registro público de paquetes suministra código basándose en un nombre indicado en un manifiesto, el ejecutor de CI realiza la instalación y el registro de artefactos almacena la imagen resultante para su posterior despliegue. Un paquete malicioso puede intentar ejecutar código durante la compilación, introducir una dependencia transitiva dañina o robar un token de publicación accesible para el ejecutor. La siguiente ruta permite visualizar dichos límites:
[fuentes][/fuentes]
Este diagrama muestra por qué la selección de dependencias es una decisión relacionada con la cadena de suministro, más que un simple detalle de codificación local. Una vez que el ejecutor (runner) descarga un paquete, su contenido pasa a formar parte de los elementos de entrada de la compilación y puede ejecutar hooks de instalación, a menos que la canalización (pipeline) los restrinja.
El archivo de manifiesto constituye la primera barrera, ya que npm install interpreta cada nombre de paquete declarado como una instrucción para resolver y descargar código. En api-service, se espera la versión 4.18.3 de express, mientras que el nombre inventado expresss (versión 1.0.0) es un error tipográfico que apunta a un paquete distinto. Este tipo de ataque se conoce como typosquatting; en él, un atacante publica un paquete cuyo nombre se asemeja al de una dependencia de confianza y se aprovecha de un error humano al seleccionarlo.
Un archivo de bloqueo (lockfile) registra la versión resuelta del paquete, los metadatos del registro y el resumen de integridad para un grafo de dependencias concreto. Dicho registro hace que las instalaciones posteriores sean reproducibles, pero solo después de que el paquete incorrecto ya haya sido aceptado y confirmado (committed). Por tanto, el fallo en la revisión se hace evidente en dos puntos: package.json contiene una dependencia directa inesperada, y package-lock.json incluye una nueva entrada resuelta y un valor de integridad. ¿Qué nombre de paquete y qué entrada en el archivo de bloqueo deberían provocar que la incorporación de expresss no supere la revisión?
Este código demuestra que una instalación exitosa no garantiza que se haya seleccionado la dependencia prevista. El instalador resolvió expresss exactamente según lo solicitado y registró el resultado en el archivo de bloqueo (lockfile); la repetición determinista de ese error sigue siendo un error. Revisar las diferencias en las dependencias implica tratar cualquier nombre desconocido, versión inesperada o entrada de integridad recién añadida como código que se incorpora a la versión publicada.
Regla de revisión Un archivo de bloqueo demuestra qué seleccionó el instalador. No demuestra que dicha selección estuviera autorizada o fuera segura.
Un nombre público mal escrito tiene su origen en el manifiesto, pero un paquete privado puede ser suplantado incluso si su nombre es correcto. La confusión de dependencias (dependency confusion) se produce cuando un proceso de compilación resuelve un paquete desde un registro no previsto; esto suele ocurrir porque un paquete público con el mismo nombre, o con una versión superior que cumple con el rango especificado, prevalece sobre un paquete interno. El atacante necesita poder publicar en un registro que consulte el proceso de compilación, no acceso al repositorio de código fuente privado.
En el caso de api-service, una dependencia interna como @acme/auth-client debe resolverse siempre a partir del registro privado de la organización. Un rango de versiones amplio permite utilizar versiones futuras que cumplan con dicho rango, mientras que la precedencia de los registros determina dónde busca el instalador. El archivo de bloqueo acota el resultado para una compilación confirmada, pero no puede corregir una asignación de registro que dirija una nueva compilación a la fuente incorrecta, ni proteger frente a una actualización deliberada del archivo de bloqueo que los revisores pasen por alto. El objetivo del control es lograr una resolución determinista desde el registro previsto para cada ámbito de paquete.
La comparación distingue el límite que traspasa cada amenaza de la evidencia que la revela: