Skip to main content

La misteriosa preocupación por la cadena de suministro del paquete npm string-width-cjs

Escrito por
feature snyk supply chain purple

3 de octubre de 2024

0 minutos de lectura

Esta historia comienza cuando Sébastien Lorber, encargado del mantenimiento de Docusaurus, el proyecto de documentación de código abierto basado en React, observa un cambio en el manifiesto del paquete en un pull request. Este es el cambio propuesto para el popular paquete npm cliui:

alias de paquetes npm en un pull request

En concreto, nos llama la atención el cambio en las dependencias de npm, que usa una sintaxis poco familiar:

  "dependencies": {
    "string-width": "^5.1.2",
    "string-width-cjs": "npm:string-width@^4.2.0",
    "strip-ansi": "^7.0.1",
    "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
    "wrap-ansi": "^8.1.0",
    "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"

La mayoría de los desarrolladores esperaría ver un rango de versiones semver en el valor de un paquete o quizá una URL de Git o basada en archivos. Sin embargo, en este caso hay una sintaxis especial con el prefijo npm:. ¿Qué significa?

¿Qué son los alias de paquetes npm?

El administrador de paquetes npm admite una función de alias que permite definir reglas de resolución personalizadas para los paquetes. Así, cada vez que se hace referencia al paquete, ya sea en el código o en el archivo de bloqueo, se resuelve con el nombre y la versión especificados por el alias.

En el caso del cambio propuesto en este pull request, el paquete string-width-cjs se resolverá como el paquete string-width en la versión ^4.2.0. Esto significa que habrá una entrada del directorio node_modules para string-width-cjs, pero con el contenido de string-width@^4.2.0; el archivo de bloqueo (package-lock.json) tendrá un comportamiento similar.

Los alias de paquetes son una función del administrador de paquetes npm que puede usarse en casos como la compatibilidad entre ESM y CJS.

Dicho esto, se puede abusar de los alias de paquetes. En un artículo y una divulgación de seguridad de 2021, Nishant Jain, embajador de Snyk, demostró cómo se podía engañar al registro oficial npmjs para que proporcionara información incorrecta sobre las dependencias mediante alias de paquetes, como parte de un problema de confusión de dependencias y seguridad de la cadena de suministro.

El pull request era inofensivo y no había riesgo de un ataque a la cadena de suministro. Sin embargo, la preocupación de Sébastien por el nombre del paquete llevó al descubrimiento de un posible riesgo de seguridad. 

Cómo detectar comportamientos sospechosos en archivos de bloqueo de npm relacionados con módulos maliciosos

Para examinar el pull request, Sébastien usó lockfile-lint. Esta herramienta revisa archivos de bloqueo como package-lock.json o yarn.lock para detectar indicios de manipulación y garantizar que no se hayan inyectado paquetes maliciosos en lugar del paquete npm original.

Al ejecutar la herramienta, aparecieron las siguientes advertencias:

npx lockfile-lint --path package-lock.json --allowed-hosts yarn npm --validate-https --validate-package-names

detected resolved URL for package with a different name: string-width-cjs
    expected: string-width-cjs
    actual: string-width

detected resolved URL for package with a different name: strip-ansi-cjs
    expected: strip-ansi-cjs
    actual: strip-ansi

detected resolved URL for package with a different name: wrap-ansi-cjs
    expected: wrap-ansi-cjs
    actual: wrap-ansi

 ✖ Error: security issues detected!

Aviso: desarrollé lockfile-lint en 2019, después de publicar un artículo en el que revelaba el problema de seguridad de los archivos de bloqueo: por qué los archivos de bloqueo de npm pueden ser un punto ciego de seguridad para inyectar módulos maliciosos.

Alerta máxima: paquetes populares con nombres parecidos en npm

A partir de los resultados anteriores de lockfile-lint, Sébastien buscó estos nombres de paquetes en npm y, para su sorpresa, descubrió que existían en el registro público de npm:

  • https://www.npmjs.com/package/string-width-cjs

  • https://www.npmjs.com/package/strip-ansi-cjs

  • https://www.npmjs.com/package/wrap-ansi-cjs

Sébastien observó que estos paquetes no solo existen en npm, sino que también presentan características sospechosas. No estaban vinculados a ningún repositorio público de código fuente, no contenían código al inspeccionarlos y se habían publicado de forma anónima, sin información personal asociada.

Al revisar el paquete npm strip-ansi-cjs, vemos que no tiene un archivo README ni un repositorio de código fuente. Sin embargo, muchos paquetes legítimos y populares presentan el mismo comportamiento.

De hecho, este paquete es popular: tiene 529 dependencias (otros paquetes que dependen de él) y 7,274 descargas semanales.

Paquete sospechoso strip-ansi-cjs en npm

Al revisar el código de strip-ansi-cjs, vemos que este paquete solo contiene un archivo: el manifiesto package.json.

Entonces, ¿por qué un paquete que no hace nada tiene tantas descargas y por qué tantos otros paquetes dependen de él?

El paquete strip-ansi-cjs no tiene código fuente

Revisemos quién es el autor de estos paquetes npm.

Los tres paquetes pertenecen a himanshutester002 y se publicaron el año pasado con números de versión generados programáticamente. Estos son algunos aspectos interesantes:

  • El paquete npm isaacs-cliui podría ser un intento de typosquatting del fork del proyecto cliui de Isaac y del paquete npm legítimo que mantiene en su espacio de nombres: @isaacs/cliui.

  • El paquete npm azure-sdk-for-net podría ser un intento de llevar a cabo una campaña de confusión de dependencias para atacar paquetes privados con el mismo nombre.

  • El paquete npm link-deep suplanta una funcionalidad popular relacionada con paquetes de utilidades como lodash y otros.

Paquetes npm a cargo de un mantenedor sospechoso en npm

También puedes observar que el usuario himanshutester002 no proporciona información que permita identificarlo en su perfil de npmjs.

Ya mencionamos que el paquete npm strip-ansi-cjs tiene más de 500 paquetes que lo usan; esto podría indicar que es popular. Veamos cuáles son:

Paquetes que dependen del registro npm

Que aparezca en la lista podría parecer convincente, pero ¿realmente lo es?
Por ejemplo, nombres como clazz-transformer, react-native-multiply o quizá gh-monoproject-cli parecen legítimos, pero ¿lo son?

Esta es la página del paquete npm react-native-multiply:

Página del paquete npm react-native-multiply que muestra 776 dependencias, el comando de instalación, código de uso y descargas semanales.

Este paquete prácticamente no tiene descargas y su autor es un usuario anónimo de npm sin información que permita identificarlo. La URL del repositorio de origen a la que redirige el paquete lleva a https://github[.]com/hasandader/react-native-multiply, un repositorio inexistente. El perfil de GitHub del usuario también parece muy sospechoso y no muestra actividad relevante.

Aunque el paquete npm parece contener código fuente, al examinarlo más de cerca vemos que es una muestra de código generada para el prototipo de una aplicación de “hola mundo”.

Explorador de archivos que muestra el directorio del proyecto /react-native-multiply/ con carpetas de Android, C++, iOS, library y source, además de archivos del paquete.

También cabe preguntarse: si este paquete es solo una biblioteca de multiplicación, ¿por qué necesita 776 dependencias para hacer lo siguiente?

import { multiply } from 'react-native-multiply';
const result = await multiply(3, 7);

Aunque algunos bromean con que JavaScript genera un árbol astronómico de paquetes anidados por el uso excesivo de dependencias, un proyecto con 776 dependencias directas es excesivamente grande.

Entre todas estas dependencias están los tres paquetes npm sospechosos con los que comenzó nuestra historia: string-width-cjs, strip-ansi-cjs y wrap-ansi-cjs:

Fragmento de código que muestra la versión ^5.1.1 del paquete npm string-width-cjs entre dependencias relacionadas con utilidades de cadenas

Mencionamos que una de las dependencias de strip-ansi-cjs se llamaba clazz-transformer. Veamos:

Página del paquete npm class-transformer que muestra su README, el comando de instalación, las dependencias, la versión, la licencia y las estadísticas de descarga.

Expliquemos qué sucede aquí. El paquete npm clazz-transformer usa deliberadamente un nombre incorrecto, pero en la página README aparece con el título class-transformer. Además, el repositorio de código fuente https://github[.]com/typestack/class-transformer no coincide con el nombre del paquete, lo que genera dudas sobre su legitimidad.

El archivo package.json del repositorio asociado typstack/class-transformer en GitHub es el siguiente:

Editor de código oscuro que muestra el manifiesto de paquetes JSON de class-transformer, versión 0.5.1, con detalles del repositorio y del módulo

El archivo package.json en GitHub no declara dependencias, pero al inspeccionar el código fuente del paquete correspondiente en npmjs, vemos las 437 dependencias incluidas en clazz-transformer. Una vez más, incluyen convenientemente los tres paquetes sospechosos *-cjs:

Vista del código de un paquete npm que muestra una lista JSON de dependencias de clazz-transformer, con nombres de paquetes y rangos de versiones

Otras consideraciones sobre los paquetes npm sospechosos

Antes de sacar más conclusiones, es importante mencionar algunas características de los paquetes npm que observamos:

  • Los paquetes de React Native parecen derivar de la herramienta de plantillas create-react-native-library. Esta herramienta también incluye la función de ejemplo predeterminada multiply en el código fuente que genera para los proyectos nuevos.

  • Los paquetes tienen estructuras de directorios, archivos y dependencias que podrían derivar de una plantilla inicial de Next.js 14, como las que se crean con npx create-next-app@14.

Nuestros colegas de Sonatype ya habían identificado casos similares de inundación de registros de código abierto con paquetes. En esos casos, el objetivo final era que los desarrolladores se otorgaran tokens Tea, de una plataforma Web3 para monetizar el software de código abierto.

La presencia de algunos archivos tea.yaml en los paquetes mencionados refuerza la hipótesis de que uno de los objetivos de esta campaña es minar tokens Tea mediante el uso indebido de Tea.

A principios de este año, el 14 de abril de 2024, un usuario del foro de Tea publicó un comentario que refuerza la preocupación por el uso indebido de Tea:

Comentario en un foro sobre el abuso de Tea: la mayoría de los proyectos son spam inútil

Antes de compartir mis conclusiones, quiero agradecer sinceramente a Sébastien Lorber por su prudencia como encargado del mantenimiento y por ayudar a descubrir estos indicios de un posible ataque a la cadena de suministro de npm.

¿Qué está pasando con string-width-cjs?

A estas alturas, tengo mucha confianza en que, si sigo investigando los demás paquetes que supuestamente dependen de string-width-cjs, encontraré indicios muy dudosos de una legitimidad auténtica.

Supongo que el único propósito de todos estos paquetes dependientes y el aumento de descargas es crear una falsa apariencia de legitimidad para los tres paquetes *-cjs, de modo que, cuando llegue el momento y se presente la víctima adecuada, se instalen estos paquetes falsos y luego aparezca una nueva versión maliciosa.

Para mantenerte protegido mientras trabajas con software de código abierto, te recomiendo adoptar prácticas de seguridad y consultar específicamente estos recursos educativos:

¿Descubrimos una campaña contra la seguridad de la cadena de suministro en medio de estas malas prácticas, o todo se reduce al rastro del dinero y, por tanto, puede atribuirse al spam y al abuso de registros públicos como npm y GitHub para minar tokens Tea?

Sea cual sea el desenlace, mantente alerta.

Protege tu código mientras desarrollas

Snyk analiza tu código para detectar problemas de calidad y seguridad, y te ofrece recomendaciones para corregirlos directamente en tu IDE.