flawopen.com/SQL इंजेक्शन/Python

Python में SQL इंजेक्शन

गंभीर CWE-89 ड्राफ़्ट — समीक्षा लंबित
Language: English Deutsch Español Français हिन्दी Bahasa Indonesia 日本語 한국어 Português (Brasil) Русский 简体中文
आसान भाषा में

एक ऐसे फ़ॉर्म की कल्पना कीजिए जो सिर्फ़ 482 जैसा टिकट नंबर मांगता है। SQL इंजेक्शन तब होता है जब कोई उस बॉक्स में नंबर की जगह कुछ चालाकी भरा टेक्स्ट डाल देता है — एक ऐसी तरकीब वाली इबारत जिससे सिस्टम "सिर्फ़ टिकट 482" दिखाने के बजाय "सारे टिकट दिखाओ" कह बैठता है, क्योंकि सिस्टम ने कभी यह जांचा ही नहीं कि उसे जो मिला वह वाकई सिर्फ़ एक नंबर था या नहीं।

इस पेज के मुख्य शब्द
यूज़र-कंट्रोल्ड इनपुट
कोई भी ऐसा मान जो अंततः ऐप इस्तेमाल करने वाले — या उस पर हमला करने वाले — व्यक्ति से आया हो: कोई फ़ॉर्म फ़ील्ड, URL पैरामीटर, अपलोड की गई फ़ाइल का नाम, या कोई HTTP हेडर। ऐप यह मान नहीं सकता कि यह सही फ़ॉर्मेट में या सुरक्षित है।
SQL क्वेरी
डेटाबेस को भेजा गया कमांड — जैसे "यह रो लाओ", "यह टेबल मिटाओ"। इसका मतलब पूरी तरह उसके सटीक टेक्स्ट से तय होता है, और यही वजह है कि उसमें अतिरिक्त टेक्स्ट डालना ख़तरनाक हो जाता है।

हो क्या रहा है

SQL इंजेक्शन तब होता है जब यूज़र-कंट्रोल्ड इनपुट को एक अलग मान के रूप में भेजने के बजाय सीधे डेटाबेस क्वेरी के टेक्स्ट में डाल दिया जाता है। अगर कोड स्ट्रिंग्स को जोड़कर क्वेरी बनाता है, तो हमलावर ऐसा इनपुट दे सकता है जो क्वेरी की असली संरचना ही बदल दे — एक "सिर्फ़ एक रो लाओ" वाली क्वेरी को ऐसी क्वेरी में बदल दे जो सारी रो लौटाए, या पूरी टेबल मिटा दे।

Python में यह लगभग हमेशा एक ही तरह दिखता है: डेटाबेस ड्राइवर के अंदर बने पैरामीटर प्लेसहोल्डर की बजाय, f-स्ट्रिंग, % फ़ॉर्मेटिंग, या सीधे + जोड़कर बनाई गई डेटाबेस कॉल।

असली दुनिया पर असर

2015 में, ब्रिटिश टेलीकॉम कंपनी TalkTalk में एक ऐसी पुरानी वेब पेज में SQL इंजेक्शन की कमज़ोरी का फ़ायदा उठाकर हमलावरों ने 1,50,000 से ज़्यादा ग्राहकों को प्रभावित करने वाली डेटा सेंध लगाई — यह पेज कंपनी को एक अधिग्रहण के ज़रिए विरासत में मिली थी। यूके के डेटा प्रोटेक्शन रेगुलेटर ने TalkTalk पर £4,00,000 का जुर्माना लगाया और इस चूक को "रोकी जा सकने वाली और बुनियादी" बताया।

स्रोत: यूके Information Commissioner's Office की एनफ़ोर्समेंट नोटिस, 2016 — नीचे "संदर्भ" देखें।

कमज़ोर बनाम ठीक किया गया कोड

कमज़ोर
# user_id सीधे रिक्वेस्ट से आता है
def get_user(cursor, user_id):
    query = f"SELECT * FROM users WHERE id = {user_id}"
    cursor.execute(query)
    return cursor.fetchone()
ठीक किया गया
# मान अलग से भेजा जाता है, कभी टेक्स्ट में नहीं जोड़ा जाता
def get_user(cursor, user_id):
    query = "SELECT * FROM users WHERE id = %s"
    cursor.execute(query, (user_id,))
    return cursor.fetchone()

यह फ़िक्स क्यों काम करता है

ठीक किए गए वर्ज़न में क्वेरी का टेक्स्ट और मान execute() को दो अलग-अलग आर्ग्युमेंट के रूप में भेजे जाते हैं। डेटाबेस ड्राइवर भी इन्हें डेटाबेस तक अलग-अलग भेजता है — मान जुड़ने से पहले ही क्वेरी की संरचना तय हो चुकी होती है, इसलिए मान में चाहे जो भी कैरेक्टर हों, उसे कभी SQL सिंटैक्स के हिस्से के रूप में नहीं समझा जा सकता। f-स्ट्रिंग ऐसा नहीं कर सकती, क्योंकि जब तक execute() क्वेरी देखता है, तब तक मान पहले ही टेक्स्ट में ऐसे शामिल हो चुका होता है जैसे वह हमेशा से कमांड का हिस्सा रहा हो।

Python से जुड़ी ख़ास बातें

execute() के अंदर % फ़ॉर्मेटिंग देखने में पैरामीटराइज़्ड लगती है — पर होती नहीं

cursor.execute("... WHERE id = %s" % user_id) उतना ही असुरक्षित है जितनी f-स्ट्रिंग। प्लेसहोल्डर तभी सुरक्षित बनता है जब मान execute() के अपने दूसरे आर्ग्युमेंट के रूप में भेजा जाए — cursor.execute("...WHERE id = %s", (user_id,)) — ताकि यह प्रतिस्थापन Python की स्ट्रिंग फ़ॉर्मेटिंग नहीं, बल्कि ड्राइवर करे।

ORM डिफ़ॉल्ट रूप से पैरामीटराइज़ करते हैं, पर उनके "एस्केप हैच" नहीं

Django का ORM और SQLAlchemy का क्वेरी बिल्डर सामान्य क्वेरी में अपने-आप पैरामीटराइज़ कर देते हैं। जैसे ही आप Model.objects.raw() या SQLAlchemy का text() इस्तेमाल करके वह रॉ SQL f-स्ट्रिंग से बनाते हैं, ख़तरा फिर लौट आता है।

प्लेसहोल्डर का सिंटैक्स हर ड्राइवर में एक जैसा नहीं होता

psycopg2 (PostgreSQL) कॉलम के टाइप की परवाह किए बिना %s इस्तेमाल करता है; sqlite3 ? इस्तेमाल करता है। एक ड्राइवर के डॉक्यूमेंटेशन से प्लेसहोल्डर स्टाइल कॉपी करके दूसरे में डालने पर चुपचाप गड़बड़ी होती है — मान लेने के बजाय अपने ख़ास ड्राइवर की पैरामीटर स्टाइल जांच लें।

आम ग़लतफ़हमियां

"मेरा ORM मुझे अपने-आप सुरक्षित रखता है"

ORM के सामान्य क्वेरी API के लिए यह सही है — पर जैसे ही आप raw() या text() से ख़ुद स्ट्रिंग बनाते हैं, यह बात ग़लत हो जाती है।

"यह ID हमेशा नंबर ही होगा, इसलिए सीधे जोड़ना सुरक्षित है"

ख़तरा रनटाइम पर मान के टाइप में नहीं है — ख़तरा इसमें है कि क्वेरी सिरे से स्ट्रिंग इंटरपोलेशन से बनाई जा रही है। कोड की ज़िंदगी में जिस भी पल यह मान्यता ग़लत साबित होगी, कमज़ोरी वहां पहले से मौजूद होगी।

"मैं ख़ुद कोट्स एस्केप करता हूं, इसलिए पैरामीटराइज़्ड क्वेरी की ज़रूरत नहीं"

मैनुअल एस्केपिंग ड्राइवर पर निर्भर होती है और उसमें बारीक़ ग़लती आसानी से हो जाती है। पैरामीटराइज़्ड क्वेरी एस्केपिंग का सख़्त वर्ज़न नहीं है — वे इस समस्या से पूरी तरह बचती हैं, क्योंकि मान कभी क्वेरी टेक्स्ट का हिस्सा बनता ही नहीं।

कैसे जांचें कि आप प्रभावित हैं या नहीं

grep -rn "execute(f\"" --include="*.py" . grep -rn "execute(.*%\s*(" --include="*.py" . grep -rn "\.raw(\|text(" --include="*.py" .
सिर्फ़ grep से बेहतर: CI में Bandit (नियम B608, hardcoded_sql_expressions) चलाएं — यह इस पैटर्न को अपने-आप पकड़ लेता है और नई घटना मिलने पर बिल्ड फ़ेल कर देता है।

रोकथाम की चेकलिस्ट

अक्सर पूछे जाने वाले सवाल

क्या ORM इस्तेमाल करने से मैं SQL इंजेक्शन से सुरक्षित हो जाता हूं?

उसके सामान्य क्वेरी मेथड के लिए, हां। लेकिन रॉ-क्वेरी वाले एस्केप हैच के लिए नहीं — वे बिल्कुल उतने ही सुरक्षित हैं जितना हाथ से लिखा SQL, उससे ज़्यादा नहीं।

क्या यह ख़तरा सिर्फ़ सर्च बॉक्स जैसी चीज़ों तक सीमित है?

नहीं। कोई भी ऐसी चीज़ जो असल में किसी बाहरी पक्ष के नियंत्रण में हो, इसमें शामिल है — HTTP हेडर, अपलोड की गई फ़ाइल के नाम, यहां तक कि किसी थर्ड-पार्टी API से आया मान जिस पर आपका ऐप भरोसा करता है।

क्या मैं बस ख़ुद कोट्स एस्केप कर लूं तो नहीं चलेगा?

कर तो सकते हैं, पर यह नाज़ुक और ड्राइवर पर निर्भर तरीक़ा है। असली फ़िक्स पैरामीटराइज़्ड क्वेरी है, एस्केपिंग का सख़्त वर्ज़न नहीं।

संदर्भ

इस भाषा में देखें: Python JavaScriptGoJava PHPC#Ruby C/C++RustKotlin Swift Solidity (लागू नहीं)
यह भी देखें: कमांड इंजेक्शनपाथ ट्रैवर्सल XSSअसुरक्षित डीसिरियलाइज़ेशन