romain-deperne/CVE-2026-41490
GitHub: romain-deperne/CVE-2026-41490
该项目披露并复现了 Dagster 数据库 I/O manager 通过动态分区键引入的 SQL 注入漏洞(CVE-2026-41490),涵盖根因分析、攻击链路和 PoC 脚本。
Stars: 0 | Forks: 0
# CVE-2026-41490 — 通过动态分区键在 Dagster 数据库 I/O manager 中进行 SQL 注入
**严重程度**:高(CVSS 8.x — AV:N/AC:L/PR:L/UI:N + C:H/I:H/A:H)
**CWE**:CWE-89 — SQL 注入
**受影响项**:`dagster` 数据库 I/O manager 集成 — `dagster-duckdb`, `dagster-snowflake`, `dagster-gcp` (BigQuery), `dagster-deltalake`, `dagster-snowflake-polars` (≤ 1.12.20)
**安全通告**:[GHSA-mjw2-v2hm-wj34](https://github.com/advisories/GHSA-mjw2-v2hm-wj34)
**NVD**:https://nvd.nist.gov/vuln/detail/CVE-2026-41490
**致谢**:Romain Deperne
## TL;DR
每个 Dagster 数据库 I/O manager 都通过**将分区键值直接通过 f-string 插值到 SQL 中**来构建其 `WHERE` 子句。当 asset 使用 `DynamicPartitionsDefinition` 时,分区键可以在运行时通过 GraphQL API (`addDynamicPartition`) 设置——这在默认的 webserver 部署中无需身份验证。恶意的分区键在未经转义的情况下流入了 `SELECT`(加载)和 `DELETE`(清理)查询中,从而导致针对后端数仓(Snowflake, BigQuery, DuckDB, DeltaLake, ……)的 SQL 注入。
## 我是如何发现此漏洞的
同一个辅助函数 `_static_where_clause` 在五个 I/O manager 包中都被复制粘贴使用。每一个都执行以下操作:
```
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 的元数据 DB 中,并在运行时通过 `addDynamicPartition` GraphQL mutation 添加。在默认的 `dagster-webserver` 部署中,GraphQL 无需身份验证,因此网络上的攻击者可以端到端地提供分区键。
我确认了该链条的两端:GraphQL mutation 在没有验证的情况下接受任意键字符串,并且该键通过 `context.asset_partition_keys` 原封不动地到达了 `_static_where_clause`。
## 攻击链
1. 获得对 Dagster webserver 的网络访问权限(默认无身份验证)
2. `addDynamicPartition(... partitionKey: "') UNION SELECT username, password_hash FROM secret_table; --")`
3. `launchRun(...)` 针对该分区
4. I/O manager 构建 `SELECT/DELETE ... WHERE col in ('') UNION SELECT ... ; --')`
5. SQL 注入在后端数据库上执行
## 受影响的代码(相同模式,5 处位置)
| 包 | 文件 |
|---------|------|
| 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-string 插值,对于动态键就变成了注入。**修复**:使用参数化查询 / 正确的标识符和字面量引用,而不是 f-string。
## 概念验证
`poc/poc_partition_sqli.py` — 展示了良性与恶意键在易受攻击的 `_static_where_clause` 中的输出对比,针对带有初始数据的数据库运行了**实时 DuckDB** 的基于 UNION 的数据提取及 `DROP TABLE`,并打印了攻击者发送的准确的 `addDynamicPartition` / `launchRun` GraphQL payload。
```
pip install duckdb # minimal; full chain: dagster dagster-duckdb pandas
python3 poc/poc_partition_sqli.py
```
## 影响
针对 Dagster 编排的数仓进行未经身份验证(默认配置)的 SQL 注入——可任意读取其他表并进行破坏性写入。Dagster 处于数据平台的核心位置,因此这将触及技术栈中最敏感的存储。
*已通过 GitHub Security Advisory 负责任地披露。概念验证在修复后发布。*
标签:Dagster, I/O管理器, 多线程, 数据库, 数据编排, 漏洞分析, 路径探测, 逆向工具