
Подробное раскрытие CVE-2026-41490 с PoC, демонстрирующим неаутентифицированную SQL-инъекцию в менеджерах ввода-вывода базы данных Dagster через динамические ключи разделов, затрагивающую интеграции DuckDB, Snowflake, BigQuery и DeltaLake.
Уровень серьёзности: Высокий (CVSS 8.3) CWE: CWE-89 — SQL-инъекция Затронуто: Dagster core <= 1.13.0 и пакеты интеграции <= 0.29.0 Исправлено в: Dagster core 1.13.1 и пакеты интеграции 0.29.1 Рекомендация: GHSA-mjw2-v2hm-wj34 NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41490 Автор находки: Romain Deperne
Каждый I/O-менеджер базы данных Dagster строит своё условие WHERE, интерполируя значения ключей
партиций напрямую в SQL через f-строки. Когда ассет использует DynamicPartitionsDefinition, ключи партиций можно
задать во время выполнения через GraphQL API (addDynamicPartition) — который не требует аутентификации в
развёртывании веб-сервера по умолчанию. Вредоносный ключ партиции попадает без экранирования как в SELECT (загрузка), так и в
DELETE (очистка) запросы, что приводит к SQL-инъекции против целевого хранилища (Snowflake, BigQuery,
DuckDB, DeltaLake, …).
Один и тот же вспомогательный метод, _static_where_clause, скопирован в пяти пакетах I/O-менеджеров. Каждый из них
делает:
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 заключён в одинарные кавычки без экранирования. Вопрос был в том, может ли partition когда-либо
контролироваться атакующим. Для статических партиций он определяется разработчиком — неинтересно. Для
DynamicPartitionsDefinition ключи хранятся в метаданных БД Dagster и добавляются во время выполнения
через GraphQL-мутацию addDynamicPartition. В развёртывании по умолчанию dagster-webserver GraphQL
не аутентифицирован, поэтому атакующий в сети передаёт ключ партиции от начала до конца.
Я подтвердил оба конца цепочки: GraphQL-мутация принимает произвольные строки ключей без
валидации, и ключ доходит до _static_where_clause дословно через context.asset_partition_keys.
addDynamicPartition(... partitionKey: "') UNION SELECT username, password_hash FROM secret_table; --")launchRun(...) с целью этой партицииSELECT/DELETE ... WHERE col in ('') UNION SELECT ... ; --')| Пакет | Файл |
|---|---|
| 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 |
Его используют как путь загрузки, так и путь очистки:
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
Ключи партиций рассматривались как доверенные константы разработчика, но DynamicPartitionsDefinition превращает их во входные данные времени выполнения, задаваемые извне. Интерполяция f-строк, которая была «безопасной» для статических ключей, становится инъекцией для динамических. Исправление: параметризованные запросы / корректное экранирование идентификаторов и литералов вместо f-строк.
poc/poc_partition_sqli.py — показывает вывод уязвимой _static_where_clause для безвредных и вредоносных ключей, выполняет живую эксфильтрацию на основе UNION в DuckDB + DROP TABLE против базы данных с предварительно заполненными данными и выводит точные GraphQL-полезные нагрузки addDynamicPartition / launchRun, которые отправляет злоумышленник.
pip install duckdb # minimal; full chain: dagster dagster-duckdb pandas
python3 poc/poc_partition_sqli.py
Неаутентифицированная SQL-инъекция (при конфигурации по умолчанию) в хранилище данных, которым оркестрирует Dagster, — произвольное чтение других таблиц и деструктивные записи. Dagster находится в центре платформ данных, поэтому это затрагивает наиболее чувствительное хранилище в стеке.
Раскрыто ответственно через GitHub Security Advisory. PoC опубликован после исправления.