
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.
Severity: High (CVSS 8.x — AV:N/AC:L/PR:L/UI:N + C:H/I:H/A:H)
CWE: CWE-89 — SQL Injection
Affected: dagster database I/O manager integrations — dagster-duckdb, dagster-snowflake, dagster-gcp (BigQuery), dagster-deltalake, dagster-snowflake-polars (≤ 1.12.20)
Advisory: GHSA-mjw2-v2hm-wj34
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41490
Credit: Romain Deperne
Every Dagster database I/O manager builds its clause by . When an asset uses , partition keys can be set at runtime through the GraphQL API () — which is unauthenticated in the default webserver deployment. A malicious partition key flows unescaped into both (load) and (cleanup) queries, yielding SQL injection against the backing warehouse (Snowflake, BigQuery, DuckDB, DeltaLake, …).
WHEREDynamicPartitionsDefinitionaddDynamicPartitionSELECTDELETEThe same helper, _static_where_clause, is copy-pasted across five I/O-manager packages. Each one
does:
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 is wrapped in single quotes with no escaping. The question was whether partition is ever
attacker-controlled. For static partitions it is developer-defined — not interesting. For
DynamicPartitionsDefinition the keys are stored in Dagster's metadata DB and added at runtime
via the addDynamicPartition GraphQL mutation. In the default dagster-webserver deployment GraphQL
is unauthenticated, so an attacker on the network supplies the partition key end-to-end.
I confirmed both ends of the chain: the GraphQL mutation accepts arbitrary key strings with no
validation, and the key reaches _static_where_clause verbatim via context.asset_partition_keys.
addDynamicPartition(... partitionKey: "') UNION SELECT username, password_hash FROM secret_table; --")launchRun(...) targeting that partitionSELECT/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 |
Both the load and cleanup paths consume it:
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
Partition keys were treated as trusted developer constants, but DynamicPartitionsDefinition turns
them into runtime, externally-settable input. The f-string interpolation that was "safe" for static
keys becomes injection for dynamic ones. Fix: parameterized queries / proper identifier+literal
quoting instead of f-strings.
poc/poc_partition_sqli.py — shows the vulnerable _static_where_clause output for benign vs.
malicious keys, runs a live DuckDB UNION-based exfiltration + DROP TABLE against a seeded
database, and prints the exact addDynamicPartition / launchRun GraphQL payloads an attacker sends.
pip install duckdb # minimal; full chain: dagster dagster-duckdb pandas
python3 poc/poc_partition_sqli.py
Unauthenticated (default config) SQL injection against the data warehouse Dagster orchestrates — arbitrary read of other tables and destructive writes. Dagster sits at the center of data platforms, so this reaches the most sensitive store in the stack.
Disclosed responsibly via GitHub Security Advisory. PoC published after the fix.