
Detailed CVE-2026-41490 disclosure with PoC demonstrating unauthenticated SQL injection in Dagster database I/O managers via dynamic partition keys, affecting DuckDB, Snowflake, BigQuery, and DeltaLake integrations.
Gravedad: Alta (CVSS 8.x — AV:N/AC:L/PR:L/UI:N + C:H/I:H/A:H)
CWE: CWE-89 — Inyección SQL
Afectados: integraciones de los gestores de E/S de base de datos de dagster — dagster-duckdb, dagster-snowflake, dagster-gcp (BigQuery), dagster-deltalake, dagster-snowflake-polars (≤ 1.12.20)
Aviso: GHSA-mjw2-v2hm-wj34
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41490
Crédito: Romain Deperne
Todos los gestores de E/S de base de datos de Dagster construyen su cláusula WHERE interpolando las claves de partición directamente en SQL mediante f-strings. Cuando un asset usa DynamicPartitionsDefinition, las claves de partición pueden establecerse en tiempo de ejecución a través de la API GraphQL (addDynamicPartition), que no está autenticada en el despliegue predeterminado del servidor web. Una clave de partición maliciosa llega sin escapar tanto a las consultas SELECT (carga) como DELETE (limpieza), lo que produce una inyección SQL contra el almacén de datos subyacente (Snowflake, BigQuery, DuckDB, DeltaLake, …).
La misma función auxiliar, _static_where_clause, se copia y pega en cinco paquetes de gestores de E/S. Cada uno hace:
def _static_where_clause(table_partition):
partitions = ", ".join(f"'{partition}'" for partition in table_partition.partitions)
return f"""{table_partition.partition_expr} in ({partitions})"""
partition se envuelve entre comillas simples sin escape. La cuestión era si partition puede estar controlado por un atacante. Para particiones estáticas, está definido por el desarrollador (no es interesante). Para DynamicPartitionsDefinition, las claves se almacenan en la base de datos de metadatos de Dagster y se añaden en tiempo de ejecución mediante la mutación GraphQL addDynamicPartition. En el despliegue predeterminado de dagster-webserver, GraphQL no está autenticado, por lo que un atacante en la red suministra la clave de partición de principio a fin.
Confirmé ambos extremos de la cadena: la mutación GraphQL acepta cadenas de clave arbitrarias sin validación, y la clave llega sin cambios a _static_where_clause a través de context.asset_partition_keys.
addDynamicPartition(... partitionKey: "') UNION SELECT username, password_hash FROM secret_table; --")launchRun(...) dirigido a esa particiónSELECT/DELETE ... WHERE col in ('') UNION SELECT ... ; --')| Paquete | Archivo |
|---|---|
| dagster-duckdb | io_manager.py:340-342 |
| dagster-snowflake | snowflake_io_manager.py:434-436 |
| dagster-gcp (BigQuery) | bigquery/io_manager.py:472-474 |
| dagster-deltalake | io_manager.py:265-267 |
| dagster-snowflake-polars | snowflake_polars_type_handler.py:74 |
Tanto la ruta de carga como la de limpieza la consumen:
query = f"SELECT {col_str} FROM {schema}.{table} WHERE\n" + _partition_where_clause(...) # read
query = f"DELETE FROM {schema}.{table} WHERE\n" + _partition_where_clause(...) # write
Las claves de partición se trataban como constantes de desarrollador de confianza, pero DynamicPartitionsDefinition las convierte en entradas de tiempo de ejecución configurables externamente. La interpolación con f-strings que era «segura» para claves estáticas se convierte en inyección para las dinámicas. Solución: consultas parametrizadas / uso correcto de comillas para identificadores y literales en lugar de f-strings.
poc/poc_partition_sqli.py — muestra la salida vulnerable de _static_where_clause para claves benignas frente a maliciosas, ejecuta una exfiltración basada en UNION contra una instancia real de DuckDB + DROP TABLE contra una base de datos precargada, e imprime las cargas útiles GraphQL exactas de addDynamicPartition / launchRun que envía un atacante.
pip install duckdb # minimal; full chain: dagster dagster-duckdb pandas
python3 poc/poc_partition_sqli.py
Inyección SQL no autenticada (configuración predeterminada) contra el almacén de datos que Dagster orquesta — lectura arbitraria de otras tablas y escrituras destructivas. Dagster se encuentra en el centro de las plataformas de datos, por lo que esto alcanza el almacén más sensible de la pila.
Divulgado de forma responsable mediante GitHub Security Advisory. PoC publicado después de la corrección.