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
- En la interfaz web de Airflow, abre el menú DAGs, elige cualquier DAG y pulsa Trigger DAG w/ config.
- Como
run_idpon el siguiente valor y lanza la ejecución:
{ti.task.__class__.__init__.__globals__[conf].__dict__}
- Ve a la página
/xcom/list/y entra en la tarea correspondiente a esa ejecución. - 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
- Cambia
try_numberpor un número de intento que no exista, por ejemplo9999:
- /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
- 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_keyde 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 trasos.path.joino 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_idde 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_numberinexistente) a menudo ejecutan código distinto y menos revisado: fuerza esos casos al buscar fallos.