Dependencias npm sin control: los 4 costes en frontends enterprise
Sonatype registró 454.600 paquetes maliciosos en 2025 y el 99,8% del malware del cuarto trimestre venía de npm. Lo que cuesta y cómo lo frenamos.
En este texto
La proliferación de dependencias npm es la distancia entre los paquetes que un equipo de frontend eligió y los que instala de verdad: un package.json con 40 entradas que en el lockfile se convierten en varios cientos, arrastradas por otra cosa. Nadie las aprobó. Nadie lee sus notas de versión. Salen a producción igual.
Donde más duele es en los frontends que duran años. Un panel interno. Un portal de clientes. Un design system que consumen otros tres equipos. El código se estabiliza, el árbol que hay debajo sigue moviéndose, y en cada instalación deciden por ti unos mantenedores con los que nunca has hablado.
¿Qué cuesta de verdad un árbol de dependencias descontrolado?
Una superficie de ataque que nadie eligió
2025 cerró el debate sobre si el riesgo era teórico. El 8 de septiembre un mantenedor cayó en un phishing montado sobre un dominio de soporte falso, npmjs.help, y 18 paquetes salieron en versiones maliciosas, entre ellos chalk y debug, con unos 2.600 millones de descargas semanales detrás (Wiz). El código malicioso corría en el navegador, esperaba window.ethereum y reescribía el destino de las transacciones de criptomonedas. Ese día las plataformas de hosting revisaron sus builds una a una: el registro de Vercel cuenta bien cómo fue.
Una semana después llegó Shai-Hulud, el primer malware de npm que se replica solo. Buscaba secretos en la máquina infectada y usaba cualquier token de npm que encontrase para publicar versiones envenenadas de todos los paquetes a los que ese token llegaba. GitHub retiró más de 500 paquetes (GitHub) y el 23 de septiembre CISA publicó una alerta (CISA).
A lo largo de 2025 Sonatype registró 454.600 paquetes maliciosos nuevos en los registros públicos, y el 99,8% del malware que interceptó en el cuarto trimestre venía solo de npm (Sonatype). En una empresa el repositorio de frontend suele ser el mayor consumidor de npm, así que carga con la mayor parte de esa exposición.
El ruido de los audits enseña a saltarse los audits
Endor Labs midió que el 95% de las dependencias vulnerables de un proyecto medio son transitivas, y que solo alrededor del 9,5% de las vulnerabilidades son explotables a nivel de función (Endor Labs). Esa proporción explica en qué acaba npm audit en un repositorio hinchado. El equipo recibe 200 avisos, resuelve la docena que está en su mano y aprende que el número de la terminal es decoración. Cuando aparece uno real, la costumbre de ignorarlo ya está instalada.
Actualizaciones bloqueadas por paquetes que no elegiste
Dos dependencias directas que fijan rangos incompatibles del mismo paquete transitivo son el final normal de esta historia. Una librería de gráficos anclada a peers viejos de React. Una librería de formularios que arrastra su propio fork de un validador. La actualización no la bloquea tu código: la bloquea un desacuerdo entre mantenedores, y el árbitro eres tú. Es el coste que aparece como un trimestre que se va, con una causa que en el manifiesto no se ve.
Tiempo de instalación y peso publicado
Las instalaciones en frío y los minutos de CI crecen con el árbol, no con tu código. También el coste de revisar: una línea cambiada en package.json llega con un diff de 2.000 líneas de lockfile que nadie lee. Y ese mismo peso acaba en el navegador. Una librería de fechas que trae todos los idiomas cuando el producto vende en tres, donde Intl.DateTimeFormat formatea los mismos valores gratis, es una decisión que se toma una vez y se paga en cada carga. Lo contamos al hablar de las puntuaciones de Lighthouse que se hunden.
Por qué la limpieza anual no aguanta
La respuesta habitual es una limpieza al año. Falla por un motivo mecánico: la poda actúa sobre las 40 líneas que controlas y el problema vive en los varios cientos que no controlas. Quita cuatro dependencias directas sin usar y el árbol casi no se mueve. El lunes siguiente una subida menor de una herramienta de build devuelve treinta paquetes, y la revisión que lo aprobó vio una sola línea.
Fijar todo falla en la dirección contraria. Si congelas el árbol dejan de llegar también los parches, y así un aviso de 2024 sobrevive en una build de 2026.
Lo que funciona no tiene ningún glamour: hacer visible el árbol, frenarlo y quitarle los privilegios que un paquete recibe gratis al instalarse.
Mide el árbol, no el manifiesto
Tres números, calculados en CI e impresos en cada pull request:
- las dependencias directas, desde
package.json - los paquetes resueltos en total, desde el lockfile (
npm ls --all --parseable | wc -l) - los paquetes que ejecutan un script al instalarse
El tercero es el que anticipa los incidentes, y casi nadie lo sigue. Una pull request que mueva cualquiera de los tres más de unos pocos puntos le debe una frase de explicación a quien revisa.
Pon un tiempo de espera en cada instalación
Hoy es el cambio con mejor relación entre valor y esfuerzo, y se escribe en una línea de configuración. Las versiones maliciosas se detectan casi siempre en horas. El daño se concentra en la ventana que va de la publicación a la retirada, y una CI que instala en cada push entra de lleno en esa ventana. Ya todos los gestores de paquetes traen su retardo:
min-release-ageen npm, en días, desde npm 11.10.0 (documentación de npm, la PR que lo introduce)minimumReleaseAgeen pnpm, en minutos, activo por defecto en 1440 (un día) desde pnpm 11 (documentación de pnpm)npmMinimalAgeGateen Yarn--minimum-release-ageen Bun
En aplicaciones usamos siete días. El filtro también se aplica a la resolución transitiva, no solo a lo que eliges a mano, y ahí está la gracia: el paquete que comprometen es justo el que no elegiste. El compromiso hay que decirlo claro. Un parche de seguridad real también espera siete días. Deja una vía de escape, baja la ventana a cero para esa instalación concreta, explícalo en el commit y devuelve el número en la misma pull request.
Desactiva los scripts de instalación por defecto
Los dos ataques de 2025 se ejecutaron a través de un script de instalación. En un frontend moderno ese paso sirve para la compilación nativa y para poco más. En CI usa npm ci --ignore-scripts y mantén una lista corta con los paquetes que compilan algo de verdad. En pnpm la misma política se escribe como onlyBuiltDependencies.
Cuenta con dos o tres paquetes roto el primer día. Casi siempre están descargando un binario ya compilado al instalarse. Son exactamente los que conviene mirar dos veces.
Deja de guardar tokens de publicación de larga vida
Si el equipo publica algo propio, un design system, un SDK interno, una CLI, en diciembre cambió el suelo. El 9 de diciembre de 2025 npm revocó todos los tokens clásicos, y npm login ahora devuelve una sesión de dos horas en lugar de una credencial duradera (changelog de GitHub). Desde el 3 de febrero de 2026 los tokens granulares de escritura tienen una vida máxima de 90 días (changelog de GitHub).
El trusted publishing por OIDC elimina el secreto guardado: el workflow demuestra su propia identidad y npm emite una credencial que vive solo para esa ejecución. Shai-Hulud se propagaba precisamente con un token robado, así que esto no es papeleo de cumplimiento: cierra ese agujero concreto.
Dale al repositorio un presupuesto de dependencias
Una entra, una sale, y se comprueba en la revisión. La pregunta en una pull request que añade un paquete no es si el paquete es bueno. Es qué sustituye. Tres comprobaciones antes de decir sí:
- ¿Cuántas entradas añade al lockfile?
npm i <pkg> --dry-runresponde en segundos. - ¿Ejecuta un script de instalación?
- ¿Qué escribiríamos en su lugar y cuánto tardaríamos?
Si la respuesta honesta a la tercera es menos de un día y el paquete trae cuarenta entradas, escribimos el código. Si hablamos de un parser, de una librería de cálculo con fechas o de una primitiva criptográfica, tomamos la dependencia y asumimos el mantenimiento. El criterio es cuánta parte del árbol puede leer el equipo de verdad.
Prioriza por alcanzabilidad, no por número de avisos
Con ese 9,5% delante, ordenar la lista de vulnerabilidades solo por gravedad tira casi todo el esfuerzo. La versión barata del análisis de alcanzabilidad es npm why <paquete>: dice qué dependencia directa metió esa cosa, que es el único sitio del que puede venir un arreglo. Después la pregunta es si tu código llega alguna vez a la función vulnerable. Si no llega, el aviso va a una lista escrita con fecha de revisión, no a este sprint.
Cómo funciona en un frontend que llevamos nosotros
Este sitio corre sobre Next.js con un design system solo en CSS. Quitar Tailwind de producción sacó del árbol una dependencia de build y toda su cadena de plugins: empezó como decisión de rendimiento y acabó siendo también de seguridad. El trabajo de servidor pasa por Server Actions en vez de una librería cliente de API. El runtime también cuenta: Bun y Node resuelven y cachean las instalaciones de forma distinta, y hoy los dos admiten el filtro por antigüedad.
Nada de esto es un ejercicio de calidad de código. Un árbol de dependencias crece por inercia y se reduce solo si alguien lo decide, así que un equipo que nunca decidió cuánto debía crecer ya lo ha decidido.
Preguntas frecuentes
¿Cuántas dependencias npm son demasiadas para un frontend?+
No hay un umbral que valga para todos los proyectos, y cualquier número que leas es la media de otro. La prueba útil es la legibilidad: ¿puede una sola persona del equipo nombrar todas las dependencias directas y decir qué hacen? Con cuarenta entradas directas normalmente sí, con noventa no. En el árbol resuelto mira la tendencia y no el valor absoluto: un lockfile que creció un 30% en un trimestre mientras el producto se quedaba igual es la señal para actuar.
¿Un tiempo de espera retrasa una semana los parches de seguridad?+
Sí, y ese es el compromiso que aceptas. Una ventana de siete días retrasa hasta siete días un arreglo real. El riesgo que quitas sigue siendo mayor que el que añades: el malware publicado se detecta en horas, mientras que las vulnerabilidades detrás de la mayoría de los avisos llevan meses ahí cuando alguien las documenta. Deja una excepción por escrito: baja la ventana a cero para esa instalación concreta, explica el motivo en el commit y devuelve el valor en la misma pull request.
¿pnpm es más seguro que npm para esto?+
pnpm te da unos valores por defecto más estrictos. Desde la versión 11 aplica una antigüedad mínima de un día sin que nadie se lo pida, y no deja que un paquete use algo que no declaró, así que un import transitivo sin declarar falla en vez de funcionar por casualidad. Con npm llegas al mismo sitio configurándolo a mano, desde la 11.10.0. Lo que importa es qué valores hereda un clon recién hecho, no la marca del gestor de paquetes.
¿Y Dependabot o Renovate abriendo actualizaciones cada día?+
Las subidas automáticas y la disciplina de dependencias tiran en direcciones opuestas si el bot no está configurado para ello. Agrupa las pull requests, una por semana y por manifiesto, dale al bot la misma ventana de espera que usas al instalar y exige el delta del lockfile en la descripción. Un bot que abre treinta pull requests por semana se aprueba sin leer, y así recreas justo el riesgo que querías reducir.
Servicios relacionados
Studio
Empieza un proyecto.
Escribimos sobre lo que construimos. Cuéntanos qué quieres construir tú.