
Divulgação detalhada da CVE-2026-41490 com PoC demonstrando injeção de SQL não autenticada nos gerenciadores de I/O de banco de dados do Dagster por meio de chaves de partição dinâmicas, afetando as integrações DuckDB, Snowflake, BigQuery e DeltaLake.
Severidade: Alta (CVSS 8.3) CWE: CWE-89 — Injeção de SQL Afetado: Dagster core <= 1.13.0 e pacotes de integração <= 0.29.0 Corrigido em: Dagster core 1.13.1 e pacotes de integração 0.29.1 Aviso: GHSA-mjw2-v2hm-wj34 NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41490 Crédito: Romain Deperne
Todo 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 via f-string. Quando um asset usa DynamicPartitionsDefinition, as chaves de partição podem
ser definidas em tempo de execução através da API GraphQL (addDynamicPartition) — que não exige autenticação na
implantação padrão do webserver. Uma chave de partição maliciosa flui sem escape tanto para consultas SELECT (carregamento) quanto
DELETE (limpeza), resultando em injeção de SQL contra o armazenamento de dados subjacente (Snowflake, BigQuery,
DuckDB, DeltaLake, …).
O mesmo helper, _static_where_clause, é copiado e colado em cinco pacotes de gerenciador 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 está entre 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 na base de dados de metadados do Dagster e adicionadas em tempo de execução
através da mutação GraphQL addDynamicPartition. Na implementação padrão do dagster-webserver o GraphQL
não tem autenticação, pelo que um atacante na rede fornece a chave de partição de ponta a ponta.
Confirmei ambos os extremos da cadeia: a mutação GraphQL aceita strings de chave arbitrárias sem
validação, e a chave chega a _static_where_clause literalmente via context.asset_partition_keys.
addDynamicPartition(... partitionKey: "') UNION SELECT username, password_hash FROM secret_table; --")launchRun(...) direcionado a essa partiçãoSELECT/DELETE ... WHERE col in ('') UNION SELECT ... ; --')| Pacote | Ficheiro |
|---|---|
| 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 como o de limpeza consomem-no:
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 eram tratadas como constantes confiáveis do desenvolvedor, mas DynamicPartitionsDefinition as transforma em entrada de runtime, configurável externamente. A interpolação com f-string que era "segura" para chaves estáticas torna-se injeção para as 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 ao vivo no DuckDB baseada em UNION + DROP TABLE contra um banco de dados pré-populado, e imprime os payloads GraphQL exatos de addDynamicPartition / launchRun que um atacante envia.
pip install duckdb # minimal; full chain: dagster dagster-duckdb pandas
python3 poc/poc_partition_sqli.py
Injeção de SQL não autenticada (configuração padrão) contra o data warehouse que o Dagster orquestra — leitura arbitrária de outras tabelas e gravações destrutivas. O Dagster está no centro das plataformas de dados, portanto isso alcança o armazenamento mais sensível da stack.
Divulgado de forma responsável via GitHub Security Advisory. PoC publicado após a correção.