flawopen.com/sql-injection/Python
Python में SQL इंजेक्शन (psycopg2, SQLAlchemy, Django)
cursor.execute() के अंदर f-स्ट्रिंग या % का इस्तेमाल रिक्वेस्ट पैरामीटर को आपका SQL बदलने देता है, और एक यूज़र की खोज पूरी टेबल के डंप में बदल जाती है (CWE-89)। यहाँ Python के हर डेटाबेस API के लिए सुरक्षित कॉल दी गई है, साथ ही वे दो मामले भी जिन्हें प्लेसहोल्डर नहीं संभाल सकते: कॉलम के नाम और IN सूचियाँ।[1], [2], [3], [4], [5], [6], [7], [9]
डाक छँटाई की मेज पर एक स्थिर निर्देश-पर्ची की कल्पना करें: ‘___ लेबल वाले पार्सल को छाँटें।’ यदि कर्मचारी पढ़ने से पहले भेजने वाले का लेबल उसी पर्ची में लिख दे, तो अतिरिक्त शब्द नए छँटाई-निर्देश बन सकते हैं। Python का f-string या % फ़ॉर्मैटिंग SQL के साथ यही करता है: driver को मान मिलने से पहले पूरा command string बन जाता है। execute(sql, params) निर्देश-पर्ची को स्थिर रखता है और लेबल को data के रूप में अलग भेजता है। Column name निर्देश का हिस्सा है; उसे code में तय allowlist से चुनें।[10], [1], [2], [9]
इस पेज के मुख्य शब्द
cursor.execute(sql, params)- DB-API 2.0 (PEP 249) का कॉल। ड्राइवर
paramsके मानों को SQL टेक्स्ट से अलग भेजता है, इसलिए वे स्टेटमेंट को कभी नहीं बदल सकते। यह तभी सुरक्षित है जब मान दूसरे आर्ग्युमेंट में हों; पहले आर्ग्युमेंट में f-स्ट्रिंग या%एक्सप्रेशन ही असली बग है।[2], [3] paramstyle: %s, ?, :name- हर ड्राइवर अपने
paramstyleएट्रिब्यूट में अपना प्लेसहोल्डर बताता है: psycopg2 और PyMySQL%sया%(name)sइस्तेमाल करते हैं, sqlite3?या:name।%sको ड्राइवर पढ़ता है, Python का%ऑपरेटर नहीं, इसलिए स्ट्रिंग को ख़ुद कभी फ़ॉर्मेट न करें।[2], [3], [5] SQLAlchemy text()- SQLAlchemy में कच्ची SQL स्ट्रिंग को लपेटने वाला फ़ंक्शन। यह तब सुरक्षित है जब मान
:emailजैसे नामित बाइंड पैरामीटर हों औरexecute()को dict के रूप में दिए जाएँ। अगर दी गई स्ट्रिंग पहले से f-स्ट्रिंग है तो असुरक्षित है:text()उसके अंदर कुछ भी एस्केप नहीं करता।[6], [9] Django raw() / RawSQL / extra()- Django के ORM से बाहर निकलने के रास्ते।
raw(sql, params)औरRawSQL(sql, params)तभी सुरक्षित हैं जब हर मानparamsमें जाए;sqlमें जो भी जोड़ा गया है, Django उसे जस का तस भेज देता है। Django के दस्तावेज़extra()को आख़िरी विकल्प बताते हैं, इसलिए रिव्यू में इस पर रोक लगाएँ।[7], [9]
फ्रेमवर्क मैपिंग
- इस गाइड में बताई गई कमजोरी के लिए CWE संदर्भ: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
हमले का चरण-दर-चरण प्रवाह
सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन
# repository.py: नीचे की हर क्वेरी रिक्वेस्ट का डेटा SQL टेक्स्ट में जोड़ती है
import sqlite3
from sqlalchemy import text
def get_user(pg_cursor, user_id):
# psycopg2: ड्राइवर के देखने से पहले ही f-स्ट्रिंग स्टेटमेंट बना देती है
pg_cursor.execute(f"SELECT id, email FROM users WHERE id = {user_id}")
return pg_cursor.fetchone()
def find_orders(conn: sqlite3.Connection, status):
# sqlite3: यह % Python की स्ट्रिंग फ़ॉर्मेटिंग है, ड्राइवर का प्लेसहोल्डर नहीं
return conn.execute("SELECT id FROM orders WHERE status = '%s'" % status).fetchall()
def find_by_email(session, email):
# SQLAlchemy: text() स्ट्रिंग में पहले से मौजूद चीज़ों को एस्केप नहीं करता
return session.execute(text(f"SELECT id FROM users WHERE email = '{email}'")).all()
def find_users(User, username):
# Django: raw() जोड़े गए मानों को डेटाबेस तक जस का तस भेजता है
return User.objects.raw(f"SELECT * FROM app_user WHERE username = '{username}'")
def get_orders_by_ids(session, ids):
# IN सूची: जोड़ी गई स्ट्रिंग में कोई तत्व अतिरिक्त SQL ले जा सकता है
id_list = ", ".join(ids)
return session.execute(text(f"SELECT id FROM orders WHERE id IN ({id_list})")).all()
def list_users(pg_cursor, sort_column):
# आइडेंटिफ़ायर: सॉर्ट कॉलम सीधे query string से आता है
pg_cursor.execute(f"SELECT id, email FROM users ORDER BY {sort_column}")
return pg_cursor.fetchall()
# repository.py: मान बाइंड पैरामीटर बनकर जाते हैं, नाम अनुमति-सूची से आते हैं
import sqlite3
from psycopg2 import sql
from sqlalchemy import bindparam, text
SORTABLE_COLUMNS = {"id", "email", "created_at"}
def get_user(pg_cursor, user_id: int):
# psycopg2: %s ड्राइवर का प्लेसहोल्डर है; मान दूसरे आर्ग्युमेंट में जाता है
pg_cursor.execute("SELECT id, email FROM users WHERE id = %s", (user_id,))
return pg_cursor.fetchone()
def find_orders(conn: sqlite3.Connection, status: str):
# sqlite3 qmark शैली अपनाता है: ? प्लेसहोल्डर, मान tuple में
return conn.execute("SELECT id FROM orders WHERE status = ?", (status,)).fetchall()
def find_by_email(session, email: str):
# SQLAlchemy: नामित बाइंड पैरामीटर, मान execute() के समय दिया जाता है
stmt = text("SELECT id FROM users WHERE email = :email")
return session.execute(stmt, {"email": email}).all()
def find_users(User, username: str):
# Django: raw() params अलग से भेजता है; ORM फ़िल्टर और भी आसान है
return User.objects.raw("SELECT * FROM app_user WHERE username = %s", [username])
def get_orders_by_ids(session, ids: list[int]):
# IN सूची: फैलने वाला bind हर तत्व के लिए एक प्लेसहोल्डर बनाता है
stmt = text("SELECT id FROM orders WHERE id IN :ids").bindparams(
bindparam("ids", expanding=True)
)
return session.execute(stmt, {"ids": ids}).all()
def list_users(pg_cursor, sort_column: str):
# आइडेंटिफ़ायर: पहले अनुमति-सूची, फिर sql.Identifier से कोट करें
if sort_column not in SORTABLE_COLUMNS:
raise ValueError(f"unsupported sort column: {sort_column!r}")
query = sql.SQL("SELECT id, email FROM users ORDER BY {}").format(sql.Identifier(sort_column))
pg_cursor.execute(query)
return pg_cursor.fetchall()
इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट
- ✓ऐसे हर
execute(),executemany(),text(),raw()याRawSQL()को रोकें जिसका SQL आर्ग्युमेंट f-स्ट्रिंग,%से फ़ॉर्मेट की गई स्ट्रिंग,str.format()कॉल या+से जोड़ी गई स्ट्रिंग हो। CI में Bandit B608 या Ruff S608 चालू करें ताकि ऐसी जगहों पर बिल्ड फ़ेल हो।[8], [14], [1], [9] - ✓हर मान ड्राइवर के पैरामीटर आर्ग्युमेंट से, उसी के paramstyle में भेजें: psycopg2 के लिए
%s, sqlite3 के लिए?, SQLAlchemy केtext()के लिए:name।[2], [3], [5] - ✓टेबल के नाम, कॉलम के नाम और सॉर्ट की दिशा सिर्फ़ कोड में तय अनुमति-सूची से लें और उन्हें
sql.Identifierसे कोट करें। इन्हें कभी सीधे रिक्वेस्ट से न पढ़ें।[4], [9] - ✓IN सूचियों को हर तत्व के लिए एक पैरामीटर से बाँधें (फैलने वाला
bindparam,= ANY(%s)या बनाए गए?मार्कर), कभी जोड़ी हुई स्ट्रिंग के रूप में नहीं।[6], [3], [5] - ✓Django में ORM फ़िल्टर को प्राथमिकता दें। पुल रिक्वेस्ट में हर
raw(),RawSQLयाconnection.cursor()को params आर्ग्युमेंट देना होगा, और नएextra()कॉल अस्वीकार किए जाएँगे।[7], [8], [9] - ✓ऐसे डेटाबेस रोल से कनेक्ट करें जिसके पास सिर्फ़ वही अधिकार हों जिनकी ऐप्लिकेशन को ज़रूरत है (DDL नहीं, सुपरयूज़र नहीं), ताकि बच निकला इंजेक्शन टेबल न मिटा सके और दूसरे स्कीमा न पढ़ सके।[9]
स्रोत
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- PEP 249 — Python Database API Specification v2.0 (paramstyle)
- psycopg2 — Passing parameters to SQL queries
- psycopg2.sql — SQL string composition
- Python sqlite3 — How to use placeholders to bind values in SQL queries
- SQLAlchemy — text() and bindparam()
- Django — Performing raw SQL queries
- Bandit B608: hardcoded_sql_expressions
- OWASP SQL Injection Prevention Cheat Sheet
- Python Language Reference — f-strings
- Flask Quickstart — request query arguments
- Django — HttpRequest.GET
- Psycopg — Multiple statements compared with psycopg2
- Ruff S608 — hardcoded SQL expression