Aparentemente, resolver una vulnerabilidad descubierta en una dependencia de nuestro proyecto podría ser tan fácil como instalar la versión parcheada. Pero hay más cosas.
De vez en cuando, herramientas como dependabot nos alertan de vulnerabilidades en nuestras dependencias y hasta es capaz de preparar una Pull Request con el parche requerido. Sin embargo, no es prudente aceptarlo tal cual.
Ya sea una dependencia directa o transitiva, la actualización de una librería podría dar lugar a problemas con nuestra aplicación. Es algo que sufrí recientemente: la versión parcheada de una cierta librería, incluía la regresión de un bug. Al actualizarla para prevenir la vulnerabilidad introduje un bug en la aplicación.
En muchos casos esto no es un gran problema: shit happens. Pero en un entorno regulado con acuerdos de servicio muy estrictos esto es un marrón mucho más gordo de lo que parece.
El proceso para resolver una vulnerabilidad
¿Es explotable?
El primer paso debería ser averiguar si la vulnerabilidad es explotable en nuestra aplicación. Es un proceso tedioso, pero conceptualmente sencillo: se trata de analizar si el código vulnerable de la librería es ejecutado directa o indirectamente por nuestra aplicación.
Ojo: esto no es para decidir si vamos a actualizar a la versión parcheada, sino para decidir cómo debemos hacer el parche de la manera correcta para no introducir problemas inesperados.
Estos son los resultados posibles:
- El código vulnerable no se ejecuta nunca: es el mejor de los casos. No estamos expuestas a la vulnerabilidad. Por tanto, podemos simplemente actualizar a la versión parcheada.
- El código vulnerable se ejecuta, pero no le llegan datos del exterior: es posible que el código se ejecute, pero nunca recibe datos provistos por un usuario de la aplicación. De este modo, la vulnerabilidad no puede ser explotada por un usuario malicioso con datos preparados. Sin embargo, al ser código que se ejecuta introduce un riesgo. No solo por la vulnerabilidad de seguridad, sino porque el parche pueda introducir un cambio de comportamiento que afecte a nuestra aplicación.
- El código vulnerable se puede ejecutar con datos del exterior: en este caso, la vulnerabilidad es explotable por cualquier usuario malicioso.
¿Podemos parchear la vulnerabilidad sin dañar la aplicación?
En el primer caso, está claro que podemos aplicar el parche sin pensarlo mucho. Al no ejecutarse el código vulnerable, tampoco corremos el riesgo de que un posible error nos afecte. ¿Podríamos llegar a utilizar ese código en el futuro? Por supuesto, pero ya gestionaremos entonces con cualquier problema que pudiera surgir.
En el segundo caso, tenemos que contar con la posibilidad de que el parche introduzca algún elemento inesperado en el comportamiento de la librería. Aunque no tengamos riesgo de seguridad, sí sabemos que el código se está usando y la actualización puede introducir cambios inesperados.
Por tanto, necesitamos tener al menos un test que ejercite la parte de la aplicación que ejecuta el código vulnerable para establecer un test de regresión que nos alerte en el caso de que la versión parcheada altere su comportamiento esperado. Este test puede existir ya, pero podríamos tener que crearlo.
De este modo, si el test falla al actualizar la librería, podemos detectar el problema y tomar medidas para solucionarlo. Podría ser modificar el código de la aplicación para evitar el problema señalado por el test. Pero también podría ser ayudarnos a escoger la versión correcta del parche.
En el tercer caso, en el que la vulnerabilidad es explotable, además de lo anterior, sería bueno tener un test que demuestre la posibilidad de explotar la vulnerabilidad en la aplicación. Este test fallará en la versión actual, indicando que se puede atacar, y solo podremos hacerlo pasar si parcheamos correctamente la librería. El test nos servirá también como evidencia de que hemos actuado para remediar la vulnerabilidad y como test de regresión en caso de que se vuelva a introducir.
Un proceso tedioso, perfecto para la IA
Todo este análisis puede ser bastante tedioso. Es el tipo de trabajo que es relativamente sencillo de hacer, pero que exige meticulosidad y exhaustividad. Es ideal para un LLM.
Efectivamente, después de haber metido la pata parcheando una vulnerabilidad para reintroducir un bug, pensé en la posibilidad de crear una skill para que Claude hiciese el trabajo duro. En realidad me bastó describirle lo que quería al asistente para que él mismo crease la skill, la cual pude poner a prueba en un par de casos más, lo que me sirvió para afinarla.
En pocas palabras, la skill consiste en[^1]:
[^1] No puedo publicar la skill, pero posiblemente bastaría con darle este artículo a Claude para que te genere una bastante apañada.
- Se le pasa la referencia de una vulnerabilidad (el código CVE o directamente el enlace al documento que la describe).
- Lo primero analizar si el código vulnerable es ejecutado por la aplicación y si, por tanto, la vulnerabilidad es explotable. Esto nos da los tres niveles de riesgo que hemos señalado antes.
- Si no es explotable, porque el código no se ejecuta, se limita a actualizar a la versión parcheada.
- Si el código se ejecuta, revisa si existe un test que pueda llegar a ejecutarlo. Si no es así, introduce uno que sí lo ejecute, describiendo el comportamiento actual que queremos preservar. Esto establece una red de regresión para el caso de que el parche rompa nuestro código.
- Si es explotable, introduce uno o más tests que lo demuestren. Estos tests fallarán hasta que se parchee.
- Para finalizar, se genera un informe breve en formato markdown para incluir en la Pull Request. Este informe detalla la vulnerabilidad, su referencia oficial y como afecta a la aplicación, así como los test que se hayan introducido.
- El commit con los cambios se hace a mano, para poder revisar todo con detalle.