Por qué todos deberían usar cooldowns de dependencias
(blog.yossarian.net)- El cooldown de dependencias (dependency cooldown) es una técnica de seguridad simple y efectiva que puede mitigar la mayoría de los ataques a la cadena de suministro de software de código abierto
- Los atacantes normalmente secuestran proyectos populares de código abierto para distribuir código malicioso, pero en la mayoría de los casos la ventana de exposición dura menos de una semana
- Si se configura un cooldown que espere cierto tiempo después de publicar una nueva versión (por ejemplo, 7 días), se puede reducir significativamente el riesgo de infección por actualizaciones automáticas
- Dependabot, Renovate y pnpm ya admiten funciones de cooldown de forma nativa, son fáciles de configurar y no tienen costo adicional
- Si el cooldown se ofrece por defecto a nivel del gestor de paquetes, puede fortalecer la seguridad de la cadena de suministro y reducir alertas innecesarias
Estructura y problema de los ataques a la cadena de suministro
- La mayoría de los ataques a la cadena de suministro (supply chain attack) siguen el mismo patrón
- El atacante obtiene acceso aprovechando el robo de credenciales o una vulnerabilidad en CI/CD de un proyecto popular de código abierto
- Sube cambios maliciosos al canal de distribución (PyPI, npm, etc.)
- Los usuarios instalan la versión infectada debido a actualizaciones automáticas o a una falta de fijación de versiones
- Un proveedor de seguridad lo detecta y emite una alerta, y luego el repositorio de paquetes elimina esa versión
- El intervalo entre las etapas (1) y (2) suele ser largo, pero de la (2) a la (5) todo ocurre en cuestión de horas o pocos días, por lo que el periodo de actividad del atacante es corto
- Ventana de oportunidad en casos importantes de los últimos 18 meses
- xz-utils: unas 5 semanas
- Ultralytics: 12 horas (etapa 1), 1 hora (etapa 2)
- tj-actions: 3 días
- chalk: menos de 12 horas
- Nx: 4 horas
- rspack: 1 hora
- num2words: menos de 12 horas
- Kong Ingress Controller: unos 10 días
- web3.js: 5 horas
- De estos casos, 8 tuvieron una duración del ataque de menos de una semana, y la mayoría podrían bloquearse con un cooldown
Concepto y efecto del cooldown
- Un cooldown retrasa el uso de una nueva dependencia durante un periodo determinado después de su publicación
- Durante ese tiempo, los proveedores de seguridad pueden detectar si es maliciosa
- Ventajas
- Es eficaz de forma empírica y bloquea la mayoría de los ataques a gran escala
- Es muy fácil de implementar y en la mayoría de las herramientas puede configurarse gratis
- Ejemplo en Dependabot
version: 2 - package-ecosystem: github-actions directory: / schedule: interval: weekly cooldown: default-days: 7 - Incentiva conductas positivas en los proveedores de seguridad: los empuja a enfocarse en la detección rápida en vez de en alertas excesivas o promoción
Conclusión y recomendaciones
- En 8 de 10 casos, los ataques duraron una semana o menos, y un cooldown de 7 días podría bloquear la mayoría
- Si se aplica un cooldown de 14 días, se podrían defender todos los casos salvo xz-utils
- El cooldown no es una solución perfecta, pero es una forma simple de reducir el riesgo de exposición entre 80% y 90%
- Además de Dependabot y Renovate, hace falta mejorar el soporte para que los propios gestores de paquetes ofrezcan cooldown por defecto
- La seguridad de la cadena de suministro no es solo un problema técnico, sino también de estructura de confianza social, pero el cooldown sigue siendo una mitigación realista y útil
3 comentarios
De hecho, si no hay problemas, creo que es mejor no actualizar innecesariamente.
¿De verdad es necesario aplicar una versión nueva que no difiere mucho de la anterior, asumiendo ese riesgo?
Comentarios de Hacker News
A la gente le preocupa quedar expuesta a vulnerabilidades graves si no actualiza de inmediato, pero en realidad casi nunca es así
Mucho software no se distribuye de forma continua, sino que el cliente instala manualmente cada nueva versión, así que se actualiza cada varias semanas o incluso meses
Lo importante es el monitoreo de dependencias y revisar las vulnerabilidades publicadas. Si se evalúa si el producto realmente está afectado, entonces solo en ese momento hace falta actualizar esa dependencia de inmediato
Se ha extendido la idea de que si sale una nueva versión, hay que actualizar sí o sí hoy mismo
Revisar sin ver los cambios reales, con la lógica de “hagámoslo ahora porque luego será más difícil”, es ineficiente
Mantenerse en la punta del versionado incluso puede ser contraproducente para la seguridad
En nuestra empresa, si el escáner detecta una vulnerabilidad crítica, hay que actualizar en un plazo de 7 días
Si se pasa el plazo, se considera incumplimiento y arranca un proceso complicado, así que la mayoría simplemente actualiza todo de inmediato
Las apps como los navegadores, que reciben mucho input externo, deben actualizarse seguido, pero en casos como una app del clima, donde el input está más limitado, el riesgo es relativamente menor
Sale más eficiente actualizar periódicamente y combinar eso con medidas de defensa contra ataques a la cadena de suministro
Un modelo como Debian stable, donde la distribución administra las dependencias comunes y se hace una actualización integral cada ciertos años, parece cada vez más razonable
Algunos ecosistemas se mueven demasiado rápido o tienen sistemas de empaquetado por distribución bastante deficientes
Por ejemplo, sigue siendo difícil instalar bibliotecas de Node.js con apt y usarlas en un proyecto
Un ecosistema que se mueve rápido sin cambios de fondo no es saludable
En JS casi no ha habido progreso real en los últimos 3 años, pero si intentas volver a compilar un proyecto de hace 3 años, el dolor es de nivel reescritura
En distribuciones como Arch, a veces ni siquiera están
Existe la idea de que poner un período de cooldown a las actualizaciones de dependencias para prevenir ataques a la cadena de suministro es mejor que usar de inmediato la versión más reciente para bloquear 0-days
Se trata de comparar la probabilidad de que una actualización introduzca una nueva vulnerabilidad frente a la probabilidad de que corrija una vulnerabilidad existente
Según SemVer, las versiones de parche son relativamente seguras, así que también se puede aplicar un cooldown corto para ese tipo de cambios
Por ejemplo, si sale la 2.4.0 estando en la 2.3.4, y no hay una función urgente, puede ser mejor esperar hasta que salga la 2.4.1
La mayoría de las vulnerabilidades no viene de ataques intencionales, sino de bugs comunes
Una política que limite la cantidad y complejidad de las dependencias es un enfoque todavía más fuerte
En lugar de agregar bibliotecas que “hacen de todo”, habría que añadir solo dependencias pequeñas y con un propósito claro
Además, si las bibliotecas ofrecieran versiones LTS que incluyan solo parches de seguridad, sería más fácil administrarlas
Reimplementar por cuenta propia puede ser un desperdicio. Da curiosidad saber qué ejemplos concretos hay de esas “everything library” problemáticas
Si confías en el mismo desarrollador a través de varias bibliotecas, importan más las relaciones de confianza que la cantidad de paquetes
La complejidad se correlaciona con las vulnerabilidades, pero no es la causa directa
Ahora es posible tomar decisiones pensando no solo en la velocidad a corto plazo, sino también en el mantenimiento a largo plazo
En el mundo de C++, este punto incluso puede ser una ventaja competitiva
La presión por actualizar siempre a la última versión viene de la creencia equivocada de que el software siempre mejora
En realidad, puede significar cambiar bugs conocidos por bugs nuevos y desconocidos
Monitorear los issues públicos y parchear solo cuando haga falta es una postura razonable
Es decir, hubo un caso en que una versión vieja terminó siendo más segura
Tal vez también sea porque ya usamos software que de por sí es bastante estable
Si todos dicen “esperemos un poco”, al final puede pasar algo parecido a una tragedia de los comunes y nadie valida primero
Cuanto más se espere, más deuda técnica se acumula, así que hacen falta actualizaciones graduales y mitigaciones como zero trust y monitoreo
Para entonces, los escáneres de seguridad ya detectaron vulnerabilidades
Si un atacante publica un release no autorizado, a veces se detecta de inmediato
La idea del cooldown es buena, pero existe el riesgo de que un atacante la explote para crear una falsa urgencia
Puede empujar una instalación temprana diciendo “parche de seguridad urgente”, cuando en realidad esa versión es maliciosa
Hay que prepararse para este tipo de ataques de presión psicológica
Otra razón para el cooldown es darles tiempo a los mantenedores para darse cuenta por sí mismos de una intrusión
Los atacantes apuntan a momentos en que el mantenedor está ausente, como vacaciones, conferencias o feriados
Muchos proyectos están mantenidos por una o dos personas, así que esta diferencia de tiempo es muy importante
Depende de las características del proyecto y de su superficie de ataque
Ya debería terminar la época de seguir simplemente las “mejores prácticas”
La seguridad es como un deporte de contacto: hay que pensar críticamente en los detalles todos los días
También existe un límite del cooldown si se asume que solo con el paso del tiempo algo se vuelve seguro
Si nadie revisa el código, una semana después el riesgo sigue siendo el mismo
En cambio, un enfoque de despliegue gradual (gradual rollout) puede ser efectivo
Cada consumidor puede configurar un factor de demora, de modo que quienes toleran más riesgo sean los primeros en encontrarse con los problemas,
mientras tanto el resto queda protegido
Las actualizaciones peligrosas se eliminan de la cola, por lo que los consumidores con retraso ni siquiera llegan a verlas
Últimamente, a veces me confundo sobre qué es más llevadero: simplemente reinventar la rueda o lidiar con el Tetris de las dependencias.
Porque, si algo sale mal por un
if for while, basta con corregir mi código, pero cuando una rueda del Tetris de dependencias se desajusta de repente, hasta depurarlo se vuelve difícil.