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管理器, 多线程, 数据库, 数据编排, 漏洞分析, 路径探测, 逆向工具