Skip to main content

Por qué los archivos de bloqueo de npm pueden ser un punto ciego de seguridad para inyectar módulos maliciosos

Escrito por

24 de septiembre de 2019

0 minutos de lectura

Hace poco empecé a explorar la idea de modelar amenazas en los paquetes del ecosistema npm. ¿Podría volver a ocurrir un incidente de event-stream? ¿Y qué hay de otros ataques a la cadena de suministro? ¿Cuál será el próximo vector de ataque que aún no hemos visto y podría ser completamente prevenible?

Y entonces, un día, ¡se me ocurrió una idea! Déjame mostrarte lo fácil que es introducir puertas traseras que los propietarios de proyectos pueden pasar por alto… y dejar tu código vulnerable.

La siguiente imagen muestra cómo inyecté un paquete malicioso mediante un pull request que hice en un paquete de código abierto de npm.

Míralo con más atención y ve si puedes detectarlo:

pull request de GitHub que muestra actualizaciones de versiones de dependencias en package.json y una diferencia contraída de yarn.lock

Ahora, así se ve mi lista de verificación para revisar pull request cuando la aplico a este PR:

  • Los paquetes que agregué son conocidos en el ecosistema y no tienen vulnerabilidades: listo.

  • Los nombres de estos paquetes no intentan suplantar otros mediante errores tipográficos: listo.

  • Son versiones válidas de esos paquetes y no son maliciosas por sí mismas: listo.

Se ve bien. ¡Adelante, hagamos merge!

Pero espera. ¿Revisaste ese archivo de bloqueo con atención? Claro que no, y no te culpo. No es nada obvio, por estas limitaciones:

  • La interfaz de GitHub oculta de forma predeterminada las diferencias cuando superan un par de cientos de líneas de código.

  • ¿Para qué revisar este archivo? Se genera automáticamente y tampoco es muy fácil de leer.

A pesar de estas limitaciones, ¿qué podría salir mal?

Expandamos las diferencias y veamos qué contienen:

pull request de GitHub que muestra un diff de yarn.lock con actualizaciones de versiones de dependencias resaltadas en rojo y verde

Cuando hay un archivo de bloqueo, ya sea el yarn.lock de Yarn o el package-lock.json de npm, durante la instalación se consulta el archivo como fuente principal de verdad para determinar las versiones de los paquetes y sus fuentes al instalar las dependencias.

¿Ves a qué me refería? Un cambio de 500 líneas en un archivo de bloqueo es, en realidad, una cantidad bastante pequeña. He visto miles de líneas de cambios en un package-lock.json de npm o en un archivo yarn.lock de Yarn. En serio, ¿quién realmente revisa miles de líneas de cambios de caracteres? Para empeorar las cosas, en este archivo de bloqueo vimos cambios en las versiones de las dependencias y actualizaciones de metadatos: en general, nada fuera de lo común.

Si bajamos un poco para revisar el resto de los cambios aplicados a este archivo, encontramos esta joya:

Diferencias de un pull request de GitHub que muestran cambios en las dependencias de yarn.lock, con las líneas eliminadas resaltadas en rojo y las agregadas en verde

Al mirar más de cerca, queda claro que reemplacé el paquete ms original de npm para resolverlo con mi propia versión, alojada en mi repositorio de GitHub. Debería haberlo obtenido del registro oficial de npm, tal como se había configurado originalmente en el archivo de bloqueo del proyecto.

Este proyecto usa varias versiones de ms, pero solo cambié ms@2.1.1 para usar mi propia versión personalizada:

yarn list --pattern ms
yarn list v1.17.3
├─ @babel/helper-module-transforms@7.4.4
├─ debug@2.6.9
│  └─ ms@2.0.0
├─ express@4.17.1
│  └─ ms@2.1.1
├─ ms@2.1.2
├─ send@0.16.2
│  └─ ms@2.0.0
├─ serve-static@1.14.1
│  └─ ms@2.1.1
✨  Done in 0.49s.

Es muy difícil acordarse de revisar con tanto detalle, ¡y no es de extrañar que tan pocas personas revisen los cambios de los archivos de bloqueo carácter por carácter!

Entonces, ¿qué hace mi propia versión de ms@2.1.1?

Cuando se integre este pull request, inyectaré mi versión maliciosa de ms@2.1.1 en el código para controlar su comportamiento durante la ejecución.

De esta manera, podría introducir una puerta trasera, alterar la lógica del módulo ms o ejecutar scripts de postinstalación. Además, la URL de alojamiento https://github.com/lirantal/ms/tarball/master es claramente sospechosa para este experimento, pero ¿te resultaría fácil detectarla si comprara un dominio con errores tipográficos para alojarlo en https://registry.npmgs.org/ms/ ?

Para este experimento, mi paquete alojado de ms tiene un script de postinstalación sencillo que muestra un texto y escribe en un archivo. Así, si instalaras el paquete, podrías confirmar que el evento posterior a la instalación se ejecuta simplemente al ejecutar yarn install:

"scripts": {
   "postinstall": "echo im installed && echo hello > /tmp/world.txt",
}

En las aplicaciones basadas en proyectos, la seguridad de los archivos de bloqueo es fundamental y está expuesta al tipo de ataques de inyección maliciosa que se describen en este artículo.

Sin embargo, los archivos de bloqueo que se incluyen en paquetes de bibliotecas están diseñados exclusivamente para usarse en el flujo de trabajo de desarrollo del propio paquete. Por eso, cuando el proyecto (paquete) se instala como dependencia en otro proyecto, no se usa su archivo de bloqueo. Esto significa que, en el caso de las bibliotecas, la inyección en archivos de bloqueo afectaría principalmente a quienes mantienen el proyecto y a sus colaboradores, y los pondría en riesgo.

¿Qué podemos hacer para mejorar la seguridad de los archivos de bloqueo?

Como los archivos de bloqueo están pensados para que los lean las máquinas, es fácil procesarlos automáticamente. Por eso, son el trabajo perfecto para un linter y algunas políticas predefinidas.

Como medida de protección contra el vector de ataque anterior, creé lockfile-lint para ayudar con esta tarea. Agregué este paso breve al análisis estático de código, que puedes ejecutar en un servidor de CI como Travis CI o Circle CI.

Como mínimo, debes validar que todos los recursos se sirvan:

  • por HTTPS

  • desde una fuente confiable

Por ejemplo, ejecutar esas verificaciones en CI para un proyecto de Yarn es muy sencillo:

$ npx lockfile-lint --path yarn.lock --type yarn --validate-https --allowed-hosts yarnpkg.org

Aquí tienes un ejemplo de errores comunes que se detectan al ejecutar el linter en el archivo de bloqueo de un proyecto:

Salida de terminal que muestra a lockfile-lint validando un archivo yarn.lock, detectando cuatro URL de paquetes que no usan HTTPS y finalizando con el código 1.

Resumen

Los ataques pueden adoptar muchas formas y tipos… ¡incluso pueden provenir de archivos de bloqueo!

Con el incidente de event-stream, vimos el primer ataque de ingeniería social exitoso contra quienes mantienen proyectos de código abierto. Quizás fue uno de los ataques más sofisticados contra la seguridad de la cadena de suministro de software que se habían visto hasta entonces en el ecosistema de npm y JavaScript.

La inyección en un archivo de bloqueo podría atacar de manera similar, por lo que debes adoptar prácticas adecuadas para mitigar este problema o, al menos, reducir el riesgo. Considera lo siguiente:

  • Revisa cuidadosamente los cambios en los archivos de bloqueo (lockfile-lint puede ayudarte).

  • Permite que solo quienes mantienen el proyecto de forma activa, y no colaboradores esporádicos, realicen cambios en los archivos de bloqueo.

  • Evita usar archivos de bloqueo en las bibliotecas.

Si te interesa leer más sobre los archivos de bloqueo y cómo se usan, escribí en detalle sobre cómo entender los archivos de bloqueo de paquetes en el ecosistema npm.

Empieza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag viendo nuestro taller virtual introductorio a pedido.

Leer más

blog feature snyk container custom base recommendations
Blog

Presentamos Evo Agentic AppSec: seguridad al ritmo del desarrollo con IA

El desarrollo impulsado por IA supera el ritmo de los flujos de trabajo de seguridad tradicionales. Agentic AppSec de Snyk usa agentes de seguridad para encontrar, corregir y verificar vulnerabilidades de forma continua, mientras las personas definen el estándar.

blog feature ai violet
Blog

VulnBump: ¿Pueden los agentes de IA corregir dependencias vulnerables sin romper la compilación?

VulnBump prueba agentes de IA en 307 actualizaciones reales de dependencias. Una corrección cuenta solo si elimina la vulnerabilidad y mantiene en verde la compilación y las pruebas.

Blog

Los modelos de frontera encontraron las vulnerabilidades. Solo el atacante encontró las cadenas.

El análisis estático encontró las fallas, pero solo las pruebas de ataque en vivo demostraron cómo podían encadenarse para provocar brechas. Una comparación de Evo COS, Claude Security y Claude Code Security.