
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.
Severidade: Alta (CVSS 8.x — AV:N/AC:L/PR:L/UI:N + C:H/I:H/A:H)
CWE: CWE-89 — Injeção SQL
Afetados: Integrações do gerenciador de I/O de banco de dados do dagster — dagster-duckdb, dagster-snowflake, dagster-gcp (BigQuery), dagster-deltalake, dagster-snowflake-polars (≤ 1.12.20)
Advertência: GHSA-mjw2-v2hm-wj34
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41490
Crédito: Romain Deperne
Cada gerenciador de I/O de banco de dados do Dagster constrói sua cláusula WHERE interpolando valores de chave de partição diretamente em SQL usando f-strings. Quando um ativo usa DynamicPartitionsDefinition, as chaves de partição podem ser definidas em tempo de execução através da API GraphQL (addDynamicPartition) — que não é autenticada na implantação padrão do servidor web. Uma chave de partição maliciosa flui sem escape para ambas as consultas SELECT (carregamento) e DELETE (limpeza), resultando em injeção SQL contra o warehouse subjacente (Snowflake, BigQuery, DuckDB, DeltaLake, …).
O mesmo helper, _static_where_clause, é copiado e colado em cinco pacotes de gerenciadores de I/O. Cada um faz:
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 é envolvido em aspas simples sem escape. A questão era se partition é alguma vez controlado pelo atacante. Para partições estáticas, é definido pelo desenvolvedor — não interessante. Para DynamicPartitionsDefinition, as chaves são armazenadas no banco de dados de metadados do Dagster e adicionadas em tempo de execução através da mutação GraphQL addDynamicPartition. Na implantação padrão do dagster-webserver, o GraphQL não é autenticado, então um atacante na rede fornece a chave de partição de ponta a ponta.
Confirmei ambas as extremidades da cadeia: a mutação GraphQL aceita strings de chave arbitrárias sem validação, e a chave chega a _static_where_clause literalmente através de context.asset_partition_keys.
addDynamicPartition(... partitionKey: "') UNION SELECT username, password_hash FROM secret_table; --")launchRun(...) direcionando essa partiçãoSELECT/DELETE ... WHERE col in ('') UNION SELECT ... ; --')| Package | File |
|---|---|
| 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 o caminho de carregamento quanto o de limpeza o consomem:
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
As chaves de partição foram tratadas como constantes confiáveis do desenvolvedor, mas DynamicPartitionsDefinition as transforma em entrada configurável externamente em tempo de execução. A interpolação de f-strings que era "segura" para chaves estáticas torna-se injeção para chaves dinâmicas. Correção: consultas parametrizadas / citação adequada de identificadores e literais em vez de f-strings.
poc/poc_partition_sqli.py — mostra a saída vulnerável de _static_where_clause para chaves benignas vs. maliciosas, executa uma exfiltração baseada em UNION no DuckDB ao vivo + DROP TABLE contra um banco de dados semeado, e imprime os payloads exatos de GraphQL addDynamicPartition / launchRun que um atacante envia.
pip install duckdb # minimal; full chain: dagster dagster-duckdb pandas
python3 poc/poc_partition_sqli.py
Injeção SQL não autenticada (configuração padrão) contra o data warehouse que o Dagster orquestra — leitura arbitrária de outras tabelas e escritas destrutivas. O Dagster está no centro das plataformas de dados, então isso atinge o armazenamento mais sensível da pilha.
Divulgado de forma responsável via GitHub Security Advisory. PoC publicado após a correção.