Tu registro privado de npm tiene un punto ciego | TuxCare

Ya están disponibles los parches de php2shell (CVE-2026-109618 y CVE-2026-109619) para PHP en fin de vida útil. Dado que las respuestas del departamento de ventas están tardando más de lo habitual debido a la elevada demanda, la forma más rápida de obtener los paquetes parcheados es comprar ELS para Runtimes en línea (se abre en una pestaña nueva). Lee las preguntas frecuentes sobre los parches

Ir directamente al resumen generado por IA de esta entrada del blog

  • Resume esta entrada con Perplexity
  • Resume esta publicación con Gemini

Principales conclusiones

  • Las dependencias de URL de Git y de archivos tar remotos se obtienen desde su propia ubicación, nunca a través de tu registro.
  • De las 53.707 versiones de paquetes que supervisamos, 811 especificaciones de dependencias se resuelven fuera del registro.
  • Las referencias de rama, etiqueta y sin fijar son el verdadero riesgo, ya que su código puede cambiar sin que se produzca una versión.
  • Una reconstrucción byte a byte demuestra la fidelidad, no la contención, por lo que un escape reproducido fielmente sigue siendo un escape.

La mayoría de los equipos que gestionan un npm El registro lo describe de la misma manera. Todo pasa por Nexus o Artifactory, el registro público se encuentra detrás de un proxy y nada llega al ordenador portátil de un desarrollador ni a un servidor de integración continua sin pasar por la única puerta que ellos controlan.

Eso es cierto, en gran medida. Es precisamente ese «en gran medida» lo que merece la pena comentar.

Mientras desarrollábamos SecureChain, el servicio de TuxCare que recompila paquetes de código abierto a partir del código fuente y los distribuye con información sobre su procedencia, nos propusimos identificar los casos en los que esto no es así. Analizamos 53.707 versiones del npm paquetes del gráfico de dependencias: hemos realizado un seguimiento y hemos marcado todas las dependencias que indican npm para prescindir por completo del registro. En esta entrada explicamos lo que hemos descubierto, por qué la configuración de tu registro no puede redirigir estas dependencias y qué debes comprobar en tus propios proyectos esta semana.

Qué entendemos por «dependencia que se escapa»

Una dependencia que no se puede rastrear es aquella que se declara mediante una URL de Git, una URL de un archivo tar o cualquier otra ubicación remota, en lugar de un nombre de paquete y un rango de versiones, por lo que npm lo recupera directamente de esa ubicación, en lugar de resolverlo a través del registro.

Aquí hay uno. Se han cambiado los nombres, pero la estructura es exactamente la misma que encontramos en las dependencias de tiempo de ejecución de un paquete publicado:

"dependencies": {
  "lodash": "^4.15.0",
  "some-markdown-lib": "git://github.com/some-user/some-markdown-lib.git"
}

La primera línea corresponde al caso normal. npm solicita al registro que hayas configurado un lodash versión que cumpla con ^4.15.0. La segunda línea no tiene versión, ni commit, ni registro. Indica un repositorio en una cuenta personal de GitHub, y npm recoge lo que haya en su rama predeterminada en el momento de la instalación.

Si eso... git:// Si te parece que el prefijo ya no funciona, tienes buen ojo. GitHub desactivó el protocolo Git sin autenticación allá por 2022. Pero eso no significa que la dependencia haya dejado de funcionar. npm normaliza GitHub git:// Las URL están en HTTPS, por lo que en cualquier entorno que permita dependencias de Git, se siguen resolviendo e instalando. Una especificación que parece defectuosa en la revisión de código funciona bien en la práctica, lo cual es una forma bastante eficaz de que algo pase desapercibido durante años.

Por qué los ajustes del Registro no pueden modificarlo

En registry instalarse .npmrc responde a una pregunta. Cuando una dependencia consiste en un nombre y un rango, ¿dónde debería npm ¿Buscarlo? Una URL de Git o un enlace a un archivo tarball HTTPS no son ese tipo de dependencia. Cada uno lleva su propia ubicación, y npm va directamente allí. Anulaciones de registro con ámbito, tokens de autenticación, un repositorio proxy delante de npmjs.org: nada de eso entra en juego, porque npm ni siquiera realiza una solicitud de registro para esa dependencia.

La profundidad tampoco ayuda. Si un paquete situado tres niveles más abajo en tu árbol declara uno, tu instalación se conecta a GitHub en mitad del proceso, desde un portátil o un «runner» que suponías que solo se comunicaba con tu registro. Cuando esas máquinas no pueden conectarse a GitHub, la instalación falla, y así es como muchos equipos se enteran de que existen. Cuando pueden conectarse, nada falla y nadie se da cuenta.

Los archivos de bloqueo no son tan útiles como la gente cree. npm Sí que registra la confirmación resuelta para una dependencia de Git, por lo que las instalaciones repetidas a partir del mismo archivo de bloqueo obtienen el mismo código. Sin embargo, la primera resolución sigue tomando lo que contenía la rama ese día, y cada consulta sigue dirigiéndose a GitHub en lugar de a tu registro. Un archivo de bloqueo fija el contenido, pero no cambia la procedencia de dicho contenido.

Lo que nos revelaron las 53.707 versiones de paquetes

Entre esas 53 707 versiones de paquetes, hemos encontrado 811 especificaciones de dependencias que se resuelven fuera del registro, repartidas entre 682 versiones de 303 paquetes distintos. GitHub concentra 730 de ellas. El resto es una mezcla heterogénea de enlaces a CDN, servidores de descarga de proveedores, alojamiento personal y un «gist» de GitHub utilizado como dependencia.

De las 811 especificaciones, 636 son dependencias de desarrollo. Los usuarios del paquete publicado no suelen instalarlas, aunque sí se descargan cada vez que se compila el propio paquete, lo cual es importante si te interesa que las compilaciones sean reproducibles.

Las otras 175 se encuentran en dependencias de tiempo de ejecución u opcionales, es decir, en los ámbitos orientados al usuario. Esa cifra debe ir acompañada de un asterisco, y es mejor señalarlo ahora que esperar a que alguien lo señale más adelante. De esas 175, 155 proceden de una única herramienta CLI abandonada hace mucho tiempo, con docenas de versiones publicadas y sin que nada en el gráfico de dependencias que seguimos dependa de ella. Si la eliminas, te quedas con 20 especificaciones de escape orientadas al usuario repartidas en 15 paquetes.

Veinte parece una cifra pequeña, y lo es. Nadie debería interpretar esto como una epidemia. Pero 13 de esos 20 apuntan a una rama o a un repositorio vacío, lo que significa que el código que hay detrás puede cambiar en cualquier momento. Las cifras pequeñas son precisamente lo que permite que este tipo de cosas pasen desapercibidas. Nadie audita los 15 paquetes de los que no sabía que eran especiales.

No todas las fugas son igual de peligrosas

La forma de la lente es mucho más importante que la dirección hacia la que apunta.

Una dependencia fijada a un SHA de commit completo se basa en el contenido. El propietario de un repositorio no puede trasladar esa referencia a otro commit, como sí puede hacer con una rama o una etiqueta, por lo que, aunque el repositorio cambie de manos mañana, obtendrás lo que había allí cuando se escribió la fijación. Lo que queda es la disponibilidad, además del hecho de que sigue eludiendo tu registro. Se trata más de un problema de contención que de inyección de código.

Las URL de repositorios sin más, las ramas, las etiquetas y las referencias Git resueltas según Semver deben considerarse mutables. Si no se especifica ninguna referencia, se entiende que se trata de la rama predeterminada. A #branch La referencia sigue a la rama. A #semver: El rango se resuelve en función de las etiquetas o referencias coincidentes en el momento de la instalación. Y las etiquetas merecen una mención aparte, porque la gente las trata como si fueran versiones. Pero no lo son. Una etiqueta es un puntero, y el propietario del repositorio puede trasladarla a otra confirmación cuando lo desee. Si no es un SHA, da por hecho que puede cambiar.

Las URL de archivos tar remotos también deberían incluirse, por lo general, en el depósito mutable, a menos que haya algún mecanismo que garantice su integridad de forma independiente. Los archivos fuente generados por GitHub han cambiado de suma de comprobación en otras ocasiones sin que nadie haya modificado el repositorio, por lo que ni siquiera un enlace que parezca fijo garantiza que contenga los mismos bytes.

En nuestro censo, la distribución fue muy desigual. Según esa clasificación, 687 de las 811 especificaciones eran mutables y solo 124 estaban vinculadas a un SHA. No contabilizamos file:, link:, workspace:, ni tampoco rutas relativas. Había 1.061 de esas, y las hemos excluido porque nunca se conectan a la red, por lo que no pueden escapar de nada.

Ahora fíjate en lo que no requiere el caso mutable. Ni apropiación de cuentas, ni typosquatting, ni token de publicación robado. El propietario de esa bifurcación puede enviar cambios a su propia rama predeterminada un martes por la tarde, y la siguiente instalación nueva en cualquier lugar la recogerá. No hay aumento de versión, ni evento de publicación, ni nada nuevo en el registro que un escáner pueda detectar. Si la URL apunta a un dominio que alguien se olvida de renovar, ni siquiera hace falta el propietario. Quien registre el dominio a continuación podrá obtener un certificado válido para él y decidir qué descarga tu instalación.

Una construcción sostenible no lo resuelve todo

Esta es la parte que más nos molestó.

Un proceso de recompilación comprueba la fidelidad. Compilamos a partir del código fuente, creamos el archivo tar y confirmamos que coincide byte a byte con el archivo tar del desarrollador original. Si coincide, sabemos que nuestra recompilación no ha modificado ni un solo byte de lo que ha publicado el desarrollador original. Esa es una garantía muy valiosa, y la defenderemos ante cualquiera.

Tampoco es la garantía que la gente suele dar por sentada. La fidelidad significa que hemos reproducido fielmente el código original. Si el manifiesto del código original indica que hay que obtener algo de una bifurcación personal, una reconstrucción fiel indica exactamente lo mismo. Un fallo reproducido fielmente sigue siendo un fallo.

La procedencia tiene los mismos límites. Una certificación SLSA indica cómo se creó un artefacto, dónde y a partir de qué fuente. No indica qué le pedirá ese artefacto al gestor de paquetes de un cliente que descargue más adelante, en otro equipo, semanas después de que haya finalizado la compilación. Ese es el alcance que SLSA define para la procedencia, y es un alcance razonable. La procedencia certifica cómo se creó algo, nunca si debería haberse creado.

En su lugar, podrías supervisar la red durante las compilaciones, y es precisamente lo que hacen algunas iniciativas de recompilación. De este modo se detectan cosas que un análisis del manifiesto nunca detectaría, como un postinstall un script que descarga un archivo binario de algún sitio. Pero solo describe tu compilación. La instalación que realmente importa se lleva a cabo en el ordenador de otra persona, más tarde, y tu proxy de compilación nunca la ve. Cada enfoque cubre un punto ciego que tiene el otro.

La lección que hemos sacado de todo esto es que la fidelidad y la contención son cuestiones distintas y requieren comprobaciones diferentes. Si se incluye una comprobación de contención en el paso de verificación, un error ya no indica qué aspecto ha fallado.

La solución que casi empeoró las cosas

La solución más obvia es duplicarlo. Copiar la dependencia de Git en el registro como un paquete en toda regla, reescribir la especificación para que incluya un rango de versiones normal y seguir adelante.

Hay una pega. En cuanto edites package.json, tu archivo tar ya no coincide con el original, y la comprobación de fidelidad lo rechaza, como es lógico. Al final, tienes que elegir entre tres cosas que te interesan: fidelidad byte a byte, contención real y paquetes que se instalen como sustitutos directos sin que nadie tenga que modificar su código. Puedes quedarte con dos.

Ponerle nombre a la copia resultó ser una trampa en sí misma. En un caso, la dependencia de Git apuntaba a una bifurcación personal de hacía años cuya package.json declaró un nombre de paquete en mayúsculas y una versión que nunca se había publicado en ningún sitio. npm No admite nombres de paquetes nuevos con letras mayúsculas, así que ese nombre quedaba descartado. El nombre en minúsculas ya estaba ocupado en el registro público, por una bifurcación diferente, con un número de versión inferior. Si publicamos nuestra copia con ese nombre y una versión superior, un resolutor configurado para comparar candidatos entre fuentes privadas y públicas podría dar preferencia a la nuestra.

Eso se parece mucho a la receta clásica para la confusión de dependencias. Habríamos creado precisamente el tipo de problema que intentábamos resolver.

Una solución clara en un caso como ese es crear un nuevo nombre con ámbito limitado que no pueda entrar en conflicto con ningún elemento público, además de reescribir el manifiesto del consumidor para que apunte a ese nombre. Supone más trabajo y significa que el paquete deja de seguir automáticamente la rama de otra persona. Hemos llegado a la conclusión de que esa última parte es una ventaja. El seguimiento silencioso de la rama predeterminada de un desconocido era la vulnerabilidad, no un servicio.

Qué debes comprobar en tus propios proyectos

Empieza por encontrarlos. npm puede hacerlo por ti con su comando de consulta:

npm query ":type(git), :type(remote)"

Esta herramienta recorre el árbol de dependencias instalado y devuelve todo lo que proviene de un repositorio de Git o de una URL remota, incluidas las dependencias transitivas. Cuando encuentre algo, te interesará saber qué resultados son directos y cuáles se encuentran tres niveles más abajo.

Si ya te has mudado a npm 12, probablemente ya te hayas topado con algunos de estos problemas por las malas. Las dependencias de Git y de URL remotas ahora están bloqueadas por defecto, y una instalación que se encuentre con una de ellas fallará hasta que las actives con allow-git o allow-remote. Es un buen valor por defecto, y ya era hora de que se introdujera. Resulta tentador configurar allow-git=all Así que el proceso vuelve a mostrarse en verde, pero eso te devuelve exactamente al punto de partida, salvo que ahora hay una línea de configuración que hace que parezca una decisión deliberada. Si las únicas dependencias de Git que necesitas están en tu propio package.json, el root El valor limita la excepción a esos casos y mantiene bloqueados los transitivos. pnpm El 11 hace prácticamente lo mismo de serie. Su blockExoticSubdeps Ahora, la configuración predeterminada es true, que bloquea las especificaciones de git y tarball en las dependencias transitivas, aunque sigue permitiendo las que tú mismo declares.

Fíjate, sin embargo, en dónde reside ese control. Es el cliente el que se niega a recuperarlas, no es que tu registro contenga nada. Tu registro privado sigue sin ver nunca estas dependencias. El cliente simplemente se niega a ir a buscarlas.

Sea cual sea el resultado de la consulta, fíjate primero en el formato. Un pin SHA en una dependencia de desarrollo suele ser un problema de contención de menor prioridad. Lo que hay que corregir es una rama, una etiqueta o la URL de un repositorio «bare» en una dependencia de tiempo de ejecución, ya sea pasando a una versión publicada, si existe, o incorporando una copia fijada bajo un nombre con ámbito de tu propiedad. Y si tus ejecutores de CI pueden acceder a github.com Dicho esto, cabe preguntarse si realmente es necesario, ya que el control de salida detecta estas recuperaciones independientemente de lo que indique el manifiesto.

¿Y qué pasa entonces con el espejo?

Nada de esto hace que los registros privados carezcan de sentido. Realizan una labor muy importante. Simplemente responden a una pregunta más limitada que la que se les plantea. «¿Proviene este paquete de algún lugar que controlamos?» no es lo mismo que «¿Todo lo que necesita este paquete proviene de algún lugar que controlamos?».

Esa segunda pregunta no dejaba de surgir mientras desarrollábamos SecureChain, y ha cambiado nuestra forma de interpretar un package.json. Ahora consideramos la forma de cada especificación de dependencia como una señal de seguridad, junto con el rango de versiones.

Si tu equipo tiene npm Las dependencias que no se pueden sustituir fácilmente: ese es el problema en el que trabaja TuxCare. SecureChain abarca paquetes que reciben mantenimiento activo, recompilados desde el código fuente con trazabilidad, mientras que Endless Lifecycle Support cubre los marcos de trabajo al final de su ciclo de vida para los que el desarrollador original ha dejado de ofrecer parches, pero de los que tu aplicación sigue dependiendo.

Preguntas frecuentes

¿Qué significa que una dependencia se salga del registro?

Significa que un paquete declara una de sus dependencias mediante una URL de Git, una URL de un archivo tar o otra ubicación remota, en lugar de un nombre y un intervalo de versiones. npm resuelve esa dependencia directamente desde la ubicación indicada en la especificación, en lugar de hacerlo a través del registro configurado en .npmrc, a menos que el cliente esté configurado para bloquear ese tipo de fuente. En cualquier caso, un registro privado o un proxy no tienen nada que decir al respecto.

¿Ofrece un registro privado de npm protección frente a las dependencias de Git?

No. El enrutamiento del registro solo se aplica a las dependencias declaradas por nombre y rango de versiones, por lo que una dependencia de Git o de una URL remota nunca pasa por tu registro privado. Lo que sí puede impedirlo es el npm el propio cliente. npm 12 bloquea por defecto las dependencias de Git y las URL remotas a través de su allow-git y allow-remote configuración, que es un elemento de control independiente del Registro.

¿Npm 12 bloquea las dependencias de Git?

Sí, por defecto. npm 12 juegos allow-git y allow-remote a none, por lo que las dependencias de Git y de las URL remotas, incluidas las transitivas, no se instalan a menos que se active esta opción. Al configurar allow-git=root solo permite las dependencias de Git declaradas en tu propio package.json, mientras que all permite su uso en cualquier parte del árbol.

¿Es más seguro fijar una dependencia de Git al SHA de una confirmación?

Sí, es mucho más seguro que una referencia a una rama o a una etiqueta, ya que el propietario no puede hacer que el hash de una confirmación apunte a código diferente. No obstante, sigue sin pasar por tu registro y depende de que el repositorio siga estando disponible, así que considéralo algo que debes tener en cuenta en lugar de algo que puedas ignorar.

¿Significa que, al tener procedencia SLSA, un paquete se puede instalar sin riesgo?

No. La procedencia SLSA certifica cómo y dónde se creó un artefacto y de qué fuente procede. No garantiza la autenticidad del código fuente en sí, ni cubre las dependencias que el paquete obtiene de fuera del registro cuando alguien lo instala posteriormente.

Resumen
Tu registro privado de npm tiene un punto ciego | TuxCare
Nombre del artículo
Tu registro privado de npm tiene un punto ciego | TuxCare
Descripción
Las dependencias de Git y URL omiten por completo tu registro privado de npm. Hemos analizado 53.707 versiones de paquetes para ver con qué frecuencia ocurre esto.
Autor
Nombre del editor
TuxCare
Logotipo de la editorial

Únase a más de 4.500 profesionales de Linux y el código abierto.

2 veces al mes. Sin spam.

Índice
Obtenga las respuestas de seguridad de código abierto que necesita

Únase a más de 4.500 profesionales de Linux y el código abierto.

2 veces al mes. Sin spam.