
إفصاح مفصّل عن CVE-2026-41490 مع إثبات مفهوم (PoC) يوضح حقن SQL غير مُصادَق عليه في مديري الإدخال/الإخراج لقاعدة بيانات Dagster عبر مفاتيح أقسام ديناميكية، مما يؤثر على تكاملات DuckDB وSnowflake وBigQuery وDeltaLake.
الخطورة: عالية (CVSS 8.3) CWE: CWE-89 — حقن SQL المتأثر: Dagster core <= 1.13.0 وحزم التكامل <= 0.29.0 تم إصلاحه في: Dagster core 1.13.1 وحزم التكامل 0.29.1 الإشعار الأمني: GHSA-mjw2-v2hm-wj34 NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41490 الفضل: Romain Deperne
كل مدير إدخال/إخراج لقاعدة بيانات في Dagster يبني جملة WHERE الخاصة به عن طريق إدراج قيم مفاتيح
الأقسام مباشرةً في SQL باستخدام f-string. عندما يستخدم أحد الأصول DynamicPartitionsDefinition، يمكن
تعيين مفاتيح الأقسام في وقت التشغيل عبر واجهة GraphQL API (addDynamicPartition) — وهي غير مُصادَق عليها في
نشر خادم الويب الافتراضي. يتدفق مفتاح قسم ضار دون تهريب إلى كلٍّ من استعلامات SELECT (التحميل) و
DELETE (التنظيف)، مما يؤدي إلى حقن SQL ضد مستودع البيانات الخلفي (Snowflake، BigQuery،
DuckDB، DeltaLake، …).
الدالة المساعدة نفسها، _static_where_clause، منسوخة ومُلصقة عبر خمس حزم لمديري الإدخال/الإخراج. كل واحدة منها
تقوم بما يلي:
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 وتُضاف في وقت التشغيل عبر طفرة GraphQL addDynamicPartition. في النشر الافتراضي لـ dagster-webserver يكون GraphQL غير مُصادَق عليه، لذا يوفّر المهاجم الموجود على الشبكة مفتاح القسم من طرف إلى طرف.
أكّدت طرفي السلسلة: طفرة GraphQL تقبل سلاسل مفاتيح عشوائية دون أي تحقق، ويصل المفتاح إلى _static_where_clause حرفيًا عبر context.asset_partition_keys.
addDynamicPartition(... partitionKey: "') UNION SELECT username, password_hash FROM secret_table; --")launchRun(...) مستهدفًا ذلك القسمSELECT/DELETE ... WHERE col in ('') UNION SELECT ... ; --')| الحزمة | الملف |
|---|---|
| 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 الذي كان "آمنًا" للمفاتيح الثابتة يصبح حقنًا للمفاتيح الديناميكية. الإصلاح: استعلامات مُعامَلة (parameterized) / اقتباس صحيح للمُعرّفات والقيم الحرفية بدلًا من f-strings.
poc/poc_partition_sqli.py — يعرض مخرجات _static_where_clause المصابة للمفاتيح الحميدة مقابل المفاتيح الخبيثة، ويشغّل DuckDB حيًّا لاستخراج البيانات عبر UNION + DROP TABLE ضد قاعدة بيانات مزوّدة ببيانات، ويطبع حمولات GraphQL الدقيقة لـ addDynamicPartition / launchRun التي يرسلها المهاجم.
pip install duckdb # minimal; full chain: dagster dagster-duckdb pandas
python3 poc/poc_partition_sqli.py
حقن SQL غير مُصادَق عليه (في الإعداد الافتراضي) ضد مستودع البيانات الذي يديره Dagster — قراءة عشوائية لجداول أخرى وكتابات مُدمِّرة. يقع Dagster في مركز منصات البيانات، لذا يصل هذا إلى أكثر المخازن حساسية في المنظومة.
تم الإفصاح عنه بمسؤولية عبر GitHub Security Advisory. نُشر PoC بعد الإصلاح.