## https://sploitus.com/exploit?id=CDA85394-5DE8-526C-A69E-987959729784
# CVE-2026-41490 โ SQL Injection in Dagster database I/O managers via dynamic partition keys
**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](https://github.com/advisories/GHSA-mjw2-v2hm-wj34)
**NVD**: https://nvd.nist.gov/vuln/detail/CVE-2026-41490
**Credit**: Romain Deperne
## TL;DR
Every Dagster database I/O manager builds its `WHERE` clause by **f-string-interpolating partition
key values directly into SQL**. When an asset uses `DynamicPartitionsDefinition`, partition keys can
be set at runtime through the GraphQL API (`addDynamicPartition`) โ which is unauthenticated in the
default webserver deployment. A malicious partition key flows unescaped into both `SELECT` (load) and
`DELETE` (cleanup) queries, yielding SQL injection against the backing warehouse (Snowflake, BigQuery,
DuckDB, DeltaLake, โฆ).
## How I found this
The same helper, `_static_where_clause`, is copy-pasted across five I/O-manager packages. Each one
does:
```python
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`.
## Attack chain
1. Network access to the Dagster webserver (no auth by default)
2. `addDynamicPartition(... partitionKey: "') UNION SELECT username, password_hash FROM secret_table; --")`
3. `launchRun(...)` targeting that partition
4. The I/O manager builds `SELECT/DELETE ... WHERE col in ('') UNION SELECT ... ; --')`
5. SQL injection executes against the backing database
## Affected code (identical pattern, 5 locations)
| 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:
```python
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
```
## Root cause
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.
## Proof of Concept
`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.
```bash
pip install duckdb # minimal; full chain: dagster dagster-duckdb pandas
python3 poc/poc_partition_sqli.py
```
## Impact
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.*