درس آموزشی درس ۲ از ۵

SQL Injection چیست؟ آموزش تزریق SQL با مثال و روش‌های جلوگیری

یک کادر جست‌وجوی ساده چطور ممکنه منطق دیتابیس رو عوض کنه؟ با آزمایش آفلاین باشگاه بازی رومیزی، SQL Injection و روش جلوگیری اصولی ازش رو یاد بگیر.

مقدماتی ۲۸ دقیقه مطالعه بروزرسانی ۲۵ مهر ۱۴۰۵
فهرست این درس

ببین، فرض کن یه باشگاه بازی رومیزی داری و برای اعضاش یه سایت رزرو راه انداختی. کاربر اسم بازی یا ژانرش رو وارد می‌کنه، روی جست‌وجو می‌زنه و نتیجه رو می‌بینه. از بیرون همه‌چیز عادیه؛ یه کادر جست‌وجو و چندتا نتیجه. اما پشت همین کادر، سرور باید از دیتابیس سؤال بپرسه. حالا اگه برنامه‌نویس متن کاربر رو بدون جدا کردن از دستور SQL وارد اون سؤال کنه، ممکنه کاربر ناخواسته یا عمداً منطق پرس‌وجو رو تغییر بده. داستان SQL Injection دقیقاً از همین اشتباه شروع می‌شه، نه از یه نرم‌افزار جادویی برای هک کردن.

توی این درس می‌خوام قدم‌به‌قدم نشونت بدم SQL Injection چیست، چرا بهش «تزریق SQL» می‌گن، چه آسیب‌هایی می‌تونه ایجاد کنه و چطور باید جلوش رو در کد بگیریم. برای اینکه فقط تعریف حفظ نکنی، با Python و SQLite یه آزمایش کاملاً آفلاین انجام می‌دیم و نتیجه نسخه ناامن رو کنار نسخه امن می‌ذاریم. آخرش هم به کدنویسی وردپرس می‌رسیم. اگه تازه وارد این موضوع شدی، اول مسیر امنیت وب نابغه‌ها و درس هک سایت چگونه انجام می‌شود رو ببین؛ این مقاله ادامه همون مسیر آموزشی محسوب می‌شه.

SQL Injection

SQL Injection چیست؟ تعریف ساده‌ای که واقعاً به درد می‌خورد

بذار با یه تعریف ساده شروع کنیم. SQL Injection یه آسیب‌پذیریه که وقتی به وجود میاد که برنامه، بخشی از ورودی غیرقابل اعتماد رو به‌جای «داده»، جزئی از «دستور SQL» تفسیر کنه. مثلاً قرار بوده از دیتابیس بپرسیم «کدوم بازی‌ها فکری هستن؟»، اما شیوه ساختن پرس‌وجو طوریه که متن واردشده می‌تونه خود سؤال رو عوض کنه. این مشکل معمولاً از کنار هم چسبوندن رشته‌ها برای ساخت Query به وجود میاد؛ البته شکل دقیقش به کد و درایور دیتابیس بستگی داره.

پس وقتی می‌پرسی تزریق SQL چیست، جواب کوتاهش اینه: ورودی کاربر نباید حق داشته باشه ساختار دستوری رو که سرور برای دیتابیس نوشته تغییر بده. این اتفاق فقط در فرم ورود رخ نمی‌ده؛ جست‌وجو، فیلتر کالا، گزارش‌ها، پنل مدیریت یا API هم می‌تونن نقطه ورود باشن. در عین حال صرف اینکه یه سایت فرم یا پایگاه داده داره، دلیل آسیب‌پذیر بودنش نیست. باید ببینیم Query واقعاً چطور ساخته و اجرا می‌شه.

یه مرجع مهم برای درک ریشه مشکل، راهنمای رسمی OWASP برای جلوگیری از SQL Injection هست. اونجا هم تأکید اصلی روی جدا موندن کد SQL از مقادیر ورودی و استفاده از پرس‌وجوهای پارامتریه. ما هم همین منطق رو یاد می‌گیریم تا اگر بعداً به‌جای Python سراغ PHP یا یک فریم‌ورک دیگه رفتی، فقط شکل نوشتن کد تغییر کنه، نه اصل امنیتش.

اصلاً SQL چیه و چه نقشی توی سایت داره؟

اول خود SQL رو بشناسیم. این زبان به برنامه کمک می‌کنه از پایگاه داده رابطه‌ای اطلاعات بگیره یا اطلاعات رو تغییر بده. مثلاً سایت باشگاه باید لیست بازی‌ها، موجودی و رزرو اعضا رو نگه داره. معمولاً مرورگر مستقیماً به دیتابیس وصل نمی‌شه؛ درخواست رو برای سرور می‌فرسته و سرور بعد از بررسی هویت، مجوز و داده‌ها، Query مناسب رو اجرا می‌کنه. از همین‌جا یه قانون مهم درمیاد: تصمیم درباره اینکه چه داده‌ای خونده یا تغییر داده بشه باید دست کد قابل اعتماد سرور باشه.

مثلاً کاربر ژانر «فکری» رو انتخاب کرده و ما فقط عنوان بازی‌های همون ژانر رو می‌خوایم. در SQLite می‌تونیم شکل دستور رو از قبل مشخص کنیم: SELECT title FROM games WHERE genre = ?. علامت سؤال جای یک مقداره؛ مقدار «فکری» جداگانه به تابع اجرای Query داده می‌شه. در این حالت دیتابیس قرار نیست متن ژانر رو به‌عنوان ادامه دستور تحلیل کنه. همین تفاوت کوچیک، کلید اصلی دفاع در برابر SQL Injection هست.

جزء درخواستدر مثال باشگاه بازیمسئولیت امنیتی
ورودی کاربرژانر مورد علاقهنباید به دستور SQL تبدیل بشه
منطق برنامهنمایش بازی‌های همان ژانرمحدوده داده و مجوز کاربر رو مشخص کنه
پرس‌وجوانتخاب عنوان بازی‌هاساختار ثابت، مقدار جداگانه
دیتابیسنگهداری بازی‌ها و رزروهااجرا فقط با سطح دسترسی ضروری
پاسخ سایتفهرست بازی‌های مجازفقط اطلاعاتی رو نمایش بده که کاربر حق دیدنش رو داره

یه نکته رو همین الان روشن کنیم: Query پارامتری فقط مشکل تزریق SQL در محل استفاده درست از پارامتر رو برطرف می‌کنه؛ جای کنترل دسترسی رو نمی‌گیره. ممکنه دستور از نظر ساختار کاملاً امن باشه، ولی سرور بدون بررسی مالکیت، رزرو یک عضو رو برای عضو دیگه نمایش بده. اونجا با مشکل Authorization طرفیم. بهتره از اول این مرزها رو تفکیک کنی تا بعدها هر افشای داده‌ای رو اشتباهی «هک دیتابیس» گزارش نکنی.

ریشه SQL Injection کجاست؟ وقتی داده تبدیل به دستور می‌شه

حالا ببین اشتباه چطور وارد کد می‌شه. برنامه‌نویس عجله داره و می‌خواد ژانر رو داخل Query قرار بده؛ برای همین از f-string استفاده می‌کنه. با ورودی عادی، دستور ساخته‌شده ظاهراً بی‌نقصه و حتی جواب صحیح می‌ده. اما برنامه دیگه تضمین نکرده که متن ورودی فقط مقدار باشه. این الگوی ساخت دستور، همون ریشه‌ایه که می‌تونه به آسیب‌پذیری SQL Injection تبدیل بشه.

user_genre = "فکری"
unsafe_sql = f"SELECT title FROM games WHERE genre = '{user_genre}'"
print(unsafe_sql)
SELECT title FROM games WHERE genre = 'فکری'

خروجی کد بالا ترسناک نیست، اتفاقاً خیلی هم معمولیه. مشکلش دقیقاً همینه: با داده سالم، باگ خودش رو نشون نمی‌ده و ممکنه مدت‌ها توی پروژه بمونه. امن بودنِ یک قطعه کد یعنی حتی وقتی ورودی عجیب، اشتباه یا کنترل‌شده از بیرون رسید، مرز داده و دستور حفظ بشه. در حمله SQL Injection مسئله اصلی همین از بین رفتن مرزه، نه اینکه کاربر حتماً یک عبارت خاص رو نوشته باشه.

بعضی‌ها برای حل این مشکل می‌گن «خب تک‌کوتیشن رو از ورودی حذف می‌کنیم». ولی این کار درمان اصولی نیست. بسته به دیتابیس و نحوه ساخت Query، حالت‌های متفاوتی وجود داره و فیلتر کردن چند کاراکتر ممکنه هم داده سالم رو خراب کنه و هم مسیرهای دیگه رو جا بندازه. راه درست اینه که از اول ساختار SQL رو ثابت نگه داریم و مقدارها رو با API مخصوص پارامترها بفرستیم، نه اینکه دنبال کامل کردن یک فهرست سیاه از ورودی‌ها باشیم.

آزمایش آفلاین SQL Injection؛ چرا یه فیلتر نتیجه اشتباه می‌ده؟

بریم یه آزمایش واقعی، اما فقط روی سیستم خودمون انجام بدیم. با ماژول sqlite3 پایتون یک دیتابیس موقت در حافظه می‌سازیم و سه بازی فرضی داخلش می‌ذاریم. بعد یک ورودی معمولی و یک ورودی غیرعادی رو با دو روش اجرا می‌کنیم: روش اشتباهِ اتصال رشته و روش درستِ پارامترگذاری. این کد نه به اینترنت وصل می‌شه، نه چیزی روی سایت دیگران امتحان می‌کنه؛ هدف فقط اینه که مکانیزم تزریق SQL رو با چشم خودت ببینی.

import sqlite3

connection = sqlite3.connect(":memory:")
try:
    connection.execute("CREATE TABLE games (title TEXT, genre TEXT)")
    connection.executemany(
        "INSERT INTO games (title, genre) VALUES (?, ?)",
        [
            ("هفت خوان", "فکری"),
            ("راز نیمه شب", "معمایی"),
            ("جمعه کنار هم", "خانوادگی"),
        ],
    )

    def unsafe_search(genre):
        sql = f"SELECT title FROM games WHERE genre = '{genre}'"
        return connection.execute(sql).fetchall()

    def safe_search(genre):
        sql = "SELECT title FROM games WHERE genre = ?"
        return connection.execute(sql, (genre,)).fetchall()

    normal_genre = "فکری"
    unusual_genre = "فکری' OR '1'='1"  # فقط برای همین دیتابیس آفلاین

    print("NORMAL UNSAFE:", unsafe_search(normal_genre))
    print("NORMAL SAFE:", safe_search(normal_genre))
    print("UNSAFE:", unsafe_search(unusual_genre))
    print("SAFE:", safe_search(unusual_genre))
finally:
    connection.close()

قبل از دیدن نتیجه، یه لحظه به تفاوت دو خط اصلی دقت کن. در روش اول متن کاربر مستقیم وسط جمله SQL قرار می‌گیره؛ یعنی موتور دیتابیس ممکنه بخشی از همون متن رو به‌عنوان شرط دستور بخونه. در روش دوم، دستور WHERE genre = ? ثابت مونده و مقدار از مسیر پارامتر بهش داده می‌شه. مهم نیست اون مقدار چه علامت‌هایی داشته باشه؛ تا وقتی درست از API استفاده کنیم، قرار نیست ساختار Query رو بازنویسی کنه.

NORMAL UNSAFE: [('هفت خوان',)]
NORMAL SAFE: [('هفت خوان',)]
UNSAFE: [('هفت خوان',), ('راز نیمه شب',), ('جمعه کنار هم',)]
SAFE: []

نتیجه رو ببین: ورودی عادی در هر دو روش فقط بازی فکری رو برمی‌گردونه. اما وقتی رشته آموزشی غیرعادی رو امتحان می‌کنیم، Query ناامن هر سه بازی رو تحویل می‌ده؛ چون بخش اضافه‌شده شرط جست‌وجو رو تغییر داده. همون رشته در روش پارامتری هیچ بازی‌ای پیدا نمی‌کنه؛ چون دیتابیس اون رو صرفاً اسم یک ژانر در نظر گرفته. این آزمایش نشون می‌ده SQL Injection چطور می‌تونه منطق یک جست‌وجو رو عوض کنه؛ درباره هیچ سایت واقعی ادعایی نمی‌کنه.

یادت باشه کد ناامن رو عمداً برای همین آزمایشگاه کوچک نوشتیم. اینکه یک ورودی در دیتابیس ساختگی ما چنین اثری داشته، اجازه امتحان کردنش روی سایت عمومی نیست. اگه خواستی تمرین رو ادامه بدی، روی همین داده‌های محلی ژانر «معمایی» یا یک رشته ناموجود رو بذار و نتیجه دو روش رو مقایسه کن. هدف یادگیری آموزش SQL Injection با درک سازوکارشه، نه حفظ کردن چند ورودی آماده.

چرا نسخه امن جواب داد؟ Prepared Statement رو ساده بفهم

حالا چرا روش دوم جواب درست داد؟ چون با Parameterized Query اول دستور SQL رو مشخص کردیم و بعد مقدار رو جدا فرستادیم. در عمل این کار باعث می‌شه رشته‌ای که کاربر وارد کرده، جای یک مقدار قرار بگیره نه جای تکه‌ای از دستور. در نتیجه حتی اگر متنش شبیه شرط SQL باشه، موتور دیتابیس اون رو ادامه دستور حساب نمی‌کنه. این مهم‌ترین قدم برای جلوگیری از SQL Injection در پرس‌وجوهای معمولیه.

اسم Prepared Statement شاید پیچیده به نظر برسه، ولی منطقش ساده‌ست: سؤال رو برنامه تعیین می‌کنه، کاربر فقط مقدارش رو می‌ده. در Python و SQLite معمولاً از ? برای جای مقدار استفاده می‌کنیم؛ توی بعضی درایورها علامت متفاوتی می‌بینی. پس «علامت سؤال» به‌تنهایی جادو نمی‌کنه؛ مهم استفاده از API درست و Bind کردن پارامترهاست. مستندات رسمی sqlite3 پایتون هم همین روش رو توصیه می‌کنه.

البته پارامترگذاری رو هم نباید اشتباه تعمیم بدیم. پارامترها معمولاً جای مقدار میان، نه اسم جدول، اسم ستون یا کلمه‌هایی مثل ASC. برای قسمت‌های ساختاری باید از انتخاب‌های محدود و از پیش تعریف‌شده استفاده کنیم یا سراغ قابلیت امن مخصوص درایور بریم. تازه اگه با یک Query کاملاً پارامتری، اطلاعات عضو دیگه‌ای رو بدون مجوز نشون بدیم، همچنان مشکل کنترل دسترسی داریم. امن بودن در برابر تزریق SQL مساوی با امنیت کل برنامه نیست.

آیا تزریق SQL فقط توی فرم ورود اتفاق می‌افته؟

نه، اصلاً قرار نیست تزریق SQL فقط پشت فرم ورود پنهان باشه. هرجایی که ورودی روی Query اثر می‌ذاره می‌تونه محل بررسی باشه: فیلتر بازی‌ها، جست‌وجوی سفارش، مرتب‌سازی گزارش، پارامترهای API یا حتی کاری که در پس‌زمینه اجرا می‌شه. مثلاً ممکنه یک مقدار از فایل وارد سیستم شده باشه و بعداً در ساخت یک دستور استفاده بشه. برای پیدا کردن ریشه خطر، مسیر حرکت داده تا محل اجرای SQL رو دنبال کن، نه فقط ظاهر صفحه رو.

بخش برنامهکار واقعی آناشتباه بالقوه‌ای که باید در کد بررسی شود
جست‌وجوی سایتپیدا کردن کتاب یا محصولچسباندن عبارت جست‌وجو به SQL
گزارش مدیریتیفیلتر سفارش‌ها بر اساس تاریخساخت مستقیم شرط‌ها از پارامترهای URL
API اپلیکیشننمایش داده برای یک حسابساخت Query با مقدار کنترل‌شده کلاینت
مرتب‌سازی جدولانتخاب صعودی یا نزولیقبول نام ستون یا جهت مرتب‌سازی بدون فهرست مجاز
پردازش پس‌زمینهخواندن داده ذخیره‌شده قبلیساخت ناامن Query از داده‌ای که بعداً از دیتابیس خوانده شده

به ردیف آخر جدول دقت کن. گاهی داده امروز وارد دیتابیس می‌شه و هیچ اتفاق غیرعادی‌ای نمی‌افته، اما فردا یک قسمت دیگه برنامه همون داده ذخیره‌شده رو ناامن وارد Query تازه‌ای می‌کنه. به این حالت Second-order SQL Injection می‌گن. اینکه داده از دیتابیس خودمون خونده شده، خودکار قابل اعتمادش نمی‌کنه؛ هر بار که قرار شد Query ساخته بشه باید همون قانون جداسازی داده و دستور رعایت بشه.

انواع SQL Injection چی هستن و چه فرقی دارن؟

وقتی درباره انواع SQL Injection می‌خونی، ممکنه با اسم‌های زیادی روبه‌رو بشی. یه نکته هست که خیلی از فهرست‌ها توضیحش نمی‌دن: همه این نام‌ها از یک جنس نیستن. مثلاً In-band و Out-of-band بیشتر درباره مسیر دریافت نتیجه‌ان، Blind درباره استنباط نتیجه از رفتار برنامه‌ست، و Second-order درباره زمان استفاده ناامن از داده حرف می‌زنه. بنابراین نباید همه‌شون رو هفت مدل کاملاً جدا و هم‌سطح فرض کنی. جدول زیر فقط نقشه‌ای برای فهم این تفاوت‌هاست.

نوع مفهومیچه چیزی از رفتار برنامه معلوم می‌شود؟نکته آموزشی
In-band (بازخورد مستقیم)نتیجه از همان کانال درخواست و پاسخ دیده می‌شودError-based و Union-based می‌تونن از روش‌های این گروه باشن
Error-basedبرخی پیام‌های خطا ممکنه سرنخ‌هایی از اجرای Query بدننوعی روش بازخورد مستقیم؛ هر خطایی نشانه تزریق نیست
Union-basedدر شرایط مناسب داده از ترکیب نتیجه پرس‌وجو قابل مشاهده استبه تعداد ستون‌ها، ساخت Query و محدودیت‌ها وابسته‌ست
Blind (استنباطی)نتیجه مستقیماً نمایش داده نمی‌شه؛ از رفتار پاسخ استنباط می‌شهشامل روش‌های بولی یا زمان‌محور در شرایط مناسب
Time-based (زمان‌محور)تفاوت زمان پاسخ گاهی به استنباط کمک می‌کنهکندی یا نوسان شبکه به‌تنهایی اثبات آسیب‌پذیری نیست
Out-of-band (کانال جدا)در بعضی شرایط، دریافت نتیجه از مسیری متفاوت انجام می‌شهبه امکانات دیتابیس و محیط بستگی داره
Second-order (مرحله دوم)داده ابتدا ذخیره و بعداً در ساخت Query ناامن استفاده می‌شهنوعی زمان وقوع استفاده ناامنه؛ نه یک کانال نتیجه مستقل

مثلاً اگر سایت هیچ پیام خطای دیتابیسی نشون نده، نمی‌تونیم بگیم در برابر SQL Injection امنه. نمایش ندادن خطای داخلی به کاربر اقدام خوبیه، اما خطای ساخت Query رو برطرف نمی‌کنه. از اون طرف، دیدن عبارت مربوط به SQL توی یک پیام خطا هم به‌تنهایی اثبات تزریق نیست؛ ممکنه یه باگ ساده یا مشکل اتصال باشه. فرق یک سرنخ با آسیب‌پذیری تأییدشده، شواهد فنی و بررسی مجازه.

این دسته‌بندی‌ها برای این خوبن که بفهمی متخصص امنیت دنبال چه نوع شاهدی می‌گرده، نه اینکه حتماً باید همه روش‌ها رو روی یک سایت اجرا کنی. در محیط تمرینیِ خودت می‌تونی رفتارها رو بدون آسیب به دیگران بررسی کنی؛ در پروژه واقعی هم مجوز، Scope و اثر آزمون باید از قبل روشن باشه. برای اطلاعات دقیق‌تر درباره روش‌های ارزیابی، بخش SQL Injection راهنمای OWASP WSTG رو ببین.

اگه از ORM استفاده کنیم، دیگه SQL Injection نداریم؟

ممکنه بگی «ما ORM داریم، پس دیگه نگرانی نداریم». کاش این‌قدر ساده بود! ORM و Query Builder اگر درست استفاده بشن کمک بزرگی به کاهش احتمال تزریق SQL می‌کنن، ولی خیلی‌هاشون امکان Raw SQL هم دارن. اگه توسعه‌دهنده اونجا متن کنترل‌نشده رو به دستور بچسبونه، مشکل برمی‌گرده. حتی در APIهای سطح بالا هم ساختن ترتیب مرتب‌سازی یا نام فیلد با ورودی خام می‌تونه دردسرساز بشه.

پس به‌جای اینکه فقط اسم Django، Laravel یا هر کتابخونه دیگه‌ای رو بپرسیم، باید مسیر ساختن Query رو بررسی کنیم. هرجا داده بیرونی وارد برنامه می‌شه، ببین آیا واقعاً به‌عنوان پارامتر ارسال شده یا یه‌جایی تبدیل به بخشی از دستور شده. این نوع بررسی خیلی ارزشمندتر از اینه که روی اسم فریم‌ورک برچسب «امن» یا «ناامن» بذاریم.

SQL Injection چیست

آیا SQL Injection فقط باعث لو رفتن اطلاعات می‌شه؟

نه؛ پیامد SQL Injection فقط لو رفتن اطلاعات نیست، ولی قرار هم نیست هر موردی رو مساوی تصاحب کل سرور بدونیم. نتیجه به نوع Query، حساب دیتابیس، سطح دسترسیش، پیکربندی و سایر کنترل‌های سیستم بستگی داره. گاهی فقط فیلتر جست‌وجو نتیجه اشتباه می‌ده، گاهی امکان دیدن یا تغییر داده‌های غیرمجاز به وجود میاد و گاهی شرایط می‌تونه جدی‌تر باشه. بدون اثبات اثر، درباره شدت و دامنه نفوذ نباید ادعای قطعی کرد.

برای همین کنار درست کردن Query، اصل کمترین سطح دسترسی رو هم جدی بگیر. مثلاً حسابی که فقط برای جست‌وجوی بازی‌های عمومی استفاده می‌شه نباید بی‌دلیل امکان حذف جدول‌ها یا مدیریت کاربران دیتابیس رو داشته باشه. این محدودسازی آسیب‌پذیری SQL Injection رو به‌خودی‌خود رفع نمی‌کنه، اما اگر جایی خطایی رخ بده، می‌تونه خسارت رو محدود کنه. امنیت خوب معمولاً حاصل چند کنترل درست در کنار همه، نه یک ابزار همه‌کاره.

SQL Injection چه فرقی با XSS و کنترل دسترسی داره؟

بذار سه اصطلاحی رو که توی آموزش هک سایت زیاد می‌شنویم کنار هم بذاریم. در SQL Injection داده غیرقابل اعتماد ممکنه ساختار دستور دیتابیس رو عوض کنه. در XSS، داده هنگام نمایش در مرورگر به محتوای اجرایی تبدیل می‌شه. در ضعف کنترل دسترسی، ممکنه کد و Query کاملاً سالم باشن اما یک کاربر اجازه انجام کاری رو بگیره که نباید داشته باشه. توی درس هک سایت این خانواده‌ها رو معرفی کردیم؛ اینجا داریم مرزشون رو دقیق‌تر می‌کنیم.

آسیب‌پذیریمرز شکسته‌شدهپرسش کلیدی برای تشخیص
SQL Injectionداده کاربر ↔ دستور دیتابیسآیا ورودی می‌تواند ساختار Query را تغییر دهد؟
XSSداده غیرقابل اعتماد ↔ کد قابل اجرا در مرورگرآیا داده به شکل اجرایی رندر می‌شود؟
Broken Access Controlهویت کاربر ↔ مجوز دسترسیآیا کاربر حق انجام این عملیات را دارد؟

فرض کن یک عضو باشگاه تونسته رزرو عضو دیگه رو ببینه. آیا حتماً تزریق SQL رخ داده؟ نه. شاید سرور اصلاً مالکیت رزرو رو کنترل نکرده. از طرف دیگه ممکنه یک Query ناامن باعث شده باشه شرط فیلتر درست اجرا نشه. خروجی ظاهری هر دو می‌تونه شبیه هم باشه، اما علت و اصلاحشون فرق داره. توی گزارش امنیتی باید بگی دقیقاً کدوم مرز شکسته شده، نه اینکه هر دسترسی غیرمجاز رو خودکار اسمش رو SQL Injection بذاری.

چطور جلوی SQL Injection رو بگیریم؟ ۵ اصل کاربردی

حالا برسیم به چیزی که برای برنامه‌نویس مهم‌تر از همه‌ست: چطور جلوی SQL Injection رو بگیریم؟ جواب پایه‌ای اینه که ساختار Query رو از ورودی جدا نگه داریم. راهنمای OWASP هم استفاده درست از Prepared Statement، اعتبارسنجی انتخاب‌های ساختاری و کمترین سطح دسترسی رو توضیح می‌ده. پنج نکته بعدی مکمل همن؛ قرار نیست یکی رو انجام بدیم و چهار تای دیگه رو فراموش کنیم.

۱. Query رو با چسبوندن متن کاربر نساز

اولین اشتباه، ساختن SQL با چسبوندن متن کاربره؛ چه با f-string توی پایتون، چه با + یا روش‌های مشابه در زبان‌های دیگه. اگه دیدی ورودی فرم مستقیم وسط دستور قرار گرفته، همون نقطه رو دقیق بررسی کن. برای مقدارها باید از قابلیت Binding در کتابخونه دیتابیس استفاده کنی. تفاوت مهمش اینه که دیگه خودت مسئول حدس زدن و Escape کردن تک‌تک کاراکترهای ویژه نیستی.

۲. ورودی رو اعتبارسنجی کن؛ ولی جای Prepared Statement نذار

اعتبارسنجی هم لازمه، اما باید نقش خودش رو داشته باشه. اگر تعداد رزرو باید عددی بین یک تا چهار باشه، ورودی خارج از این محدوده رو رد کن. اگه گزینه مرتب‌سازی فقط «نام» یا «تاریخ»ه، انتخاب سومی رو نپذیر. ولی حتی وقتی ورودی ظاهراً معتبره، مقدارش باید از مسیر امن وارد Query بشه. اعتبارسنجی جایگزین پرس‌وجوی پارامتری برای جلوگیری از تزریق SQL نیست؛ این دو کنار هم کمک می‌کنن.

۳. برای اسم ستون و مرتب‌سازی از فهرست مجاز استفاده کن

یه جای حساس دیگه هم هست: نام ستون و جهت مرتب‌سازی. نمی‌تونی همیشه اسم ستون رو مثل مقدار معمولی با ? به دیتابیس بدی. فرض کن کاربر می‌خواد بازی‌ها بر اساس عنوان یا ژانر مرتب بشن؛ بهتره ورودی اون فقط کلید title یا genre باشه و برنامه از فهرست ثابت خودش نام ستون رو انتخاب کنه. این‌طوری کنترل ساختار Query دست کد خودمونه، نه متن خام کاربر.

# فقط انتخاب بین ساختارهای مجاز، نه دریافت دستور SQL از کاربر
sort_options = {
    "title": "title",
    "genre": "genre",
}
requested_sort = "title"
column = sort_options.get(requested_sort, "title")
query = f"SELECT title, genre FROM games ORDER BY {column}"
print(query)
SELECT title, genre FROM games ORDER BY title

توی نمونه بالا به تفاوت این دو نگاه کن: requested_sort می‌تونه از تنظیمات کاربر بیاد، اما column فقط از بین دو مقدار ثابت و تعریف‌شده در کد انتخاب می‌شه. برای همین استفاده ازش در ساختار ORDER BY با چسبوندن مستقیم یک عبارت دلخواه فرق داره. اگه قراره چنین کاری در پروژه واقعی انجام بدی، فهرست مجاز، کنترل دسترسی و سازگاری با درایور دیتابیس رو هم بررسی کن.

۴. مجوز دیتابیس رو محدود کن و خطاها رو درست مدیریت کن

حساب دیتابیس رو هم با دسترسی مدیرکل راه ننداز، مگر واقعاً دلیل مشخصی داشته باشی. یک قابلیت فقط‌خواندنی نباید دسترسی حذف و تغییر نامحدود داشته باشه. از اون طرف، خطاهای حساس دیتابیس رو مستقیم جلوی کاربر نمایش نده؛ گزارش فنی باید در لاگ مناسب و بدون ذخیره بی‌دلیل رمز یا داده محرمانه ثبت بشه. این‌ها جای اصلاح کد رو نمی‌گیرن، اما برای کاهش اثر حمله SQL Injection و فهمیدن اتفاقات مهم هستن.

۵. روی WAF و اسکنر به‌تنهایی حساب نکن

فایروال برنامه وب یا WAF می‌تونه برخی درخواست‌های مشکوک رو متوقف کنه، اما خطای اصلی رو از سورس‌کد پاک نمی‌کنه. اسکنر هم ممکنه هشدار اشتباه بده یا یک مسیر رو نبینه. پس اگر خروجی ابزار گفت احتمال تزریق SQL وجود داره، باید در محیط مجاز و با شواهد بررسیش کنیم؛ اگر چیزی پیدا نکرد هم نباید فوری بگیم سایت صددرصد امنه. تفاوت هشدار و یافته تأییدشده رو در درس اسکن آسیب‌پذیری مفصل‌تر گفتیم.

آموزش SQL Injection

SQL Injection در وردپرس؛ موقع نوشتن افزونه حواست به چی باشه؟

بریم سراغ وردپرس؛ چون این بخش برای کسی که قالب یا افزونه اختصاصی می‌نویسه خیلی کاربردیه. هسته وردپرس APIهایی برای کار با دیتابیس داره، اما این به معنی امن شدن خودکار همه Queryهای یک افزونه نیست. مثلاً اگر برای گزارش فروش یا جست‌وجوی سفارشی دستور SQL دستی بنویسی، باید دقیقاً بدونی مقدارها کجا وارد می‌شن. از طرفی هم وردپرسی بودنِ سایت، به‌تنهایی نشونه وجود SQL Injection نیست؛ باید کد یا شواهد واقعی رو دید.

در وردپرس برای این کار متد $wpdb->prepare() رو داریم. %s برای مقدار رشته‌ای و %d برای مقدار عددی استفاده می‌شه؛ از وردپرس ۶٫۲ به بعد هم %i برای شناسه‌هایی مثل نام جدول پشتیبانی می‌شه. مثال پایین روی یک جدول فرضی به نام club_games نوشته شده، یعنی روی سایت معمولی وردپرس بدون ساختن این جدول قابل اجرا نیست. منبع دقیق جزئیات هم مستند رسمی wpdb::prepare هست.

<?php
// نمونه آموزشی داخل افزونه وردپرس؛ جدول club_games باید از قبل ساخته شده باشه.
// این کد به WordPress 6.2 یا WPDB با پشتیبانی از %i نیاز داره.
global $wpdb;

$table_name = $wpdb->prefix . 'club_games';
$genre = 'فکری';

if ( ! $wpdb->has_cap( 'identifier_placeholders' ) ) {
    throw new RuntimeException( 'Identifier placeholders are not supported.' );
}

$sql = $wpdb->prepare(
    'SELECT title FROM %i WHERE genre = %s',
    $table_name,
    $genre
);
$results = $wpdb->get_results( $sql );

توی این نمونه، نام جدول از پیشوند دیتابیس وردپرس و نام ثابتی که خود افزونه تعیین کرده ساخته می‌شه، و مقدار ژانر جدا به prepare() می‌ره. قبل از استفاده از %i هم پشتیبانی از identifier_placeholders بررسی می‌شه. البته این فقط نمایش درستِ ساخت Query در محیط وردپرسه؛ جدول، اتصال واقعی، سطح دسترسی و نتیجه پرس‌وجو باید در پروژه واقعی جدا آزمایش بشن. حواست باشه قالب Placeholderهای وردپرس با SQLite پایتون فرق داره.

چطور یک گزارش دقیق درباره SQL Injection بنویسیم؟

حالا فرض کن توی یک پروژه مجاز، جست‌وجوی بازی‌ها به‌جای نتیجه مورد انتظار چند ردیف اضافه برگردونده. گزارش حرفه‌ای این نیست که فوراً بنویسیم «کل دیتابیس هک شد». بهتره دقیق مشخص کنیم کجا بررسی انجام شده، ورودی چه بوده، انتظارمون چی بوده، در عمل چی دیدیم و کدوم بخش از علت رو با شواهد تأیید کردیم. شدت واقعی هم باید با توجه به دامنه دسترسی و شرایط محیط تعیین بشه.

قسمت گزارشنمونه آموزشی برای باشگاه فرضی
عنواناحتمال تغییر منطق فیلتر ژانر به‌علت ساخت ناامن Query
محیطآزمایشگاه محلی با دیتابیس داخل حافظه
رفتار مورد انتظارنمایش فقط بازی‌های همان ژانر
مشاهدهیک ورودی خاص در نسخه ناامن نتایج اضافی برگردانده
اثر تأییدشدهانحراف منطق جست‌وجو روی داده‌های ساختگی
چیزی که تأیید نشدهدسترسی به داده واقعی یا اثر در سامانه Production
پیشنهاد اصلاحاستفاده از Query پارامتری، مرور مجوزها و آزمون رگرسیون
بررسی مجددمقایسه خروجی نسخه امن با ورودی‌های عادی و نامعمول

جدول گزارش رو عمداً با سناریوی آفلاین باشگاه پر کردیم تا فرق مشاهده و نتیجه‌گیری رو ببینی. این آزمایش به ما اجازه می‌ده بگیم «دستور ساخته‌شده ناامن در دیتابیس ساختگی شرط جست‌وجو رو تغییر داد»، نه اینکه بگیم یک سایت واقعی SQL Injection داره. همین دقت در انتخاب کلمات باعث می‌شه گزارش برای تیم توسعه قابل اعتماد باشه و اصلاحیه درست پیشنهاد بده.

۵ اشتباه رایج موقع یادگیری تزریق SQL

اشتباه اول: هر خطای دیتابیس رو SQL Injection بدونیم

هر خطای SQL مساوی حمله نیست. ممکنه ستون رو غلط نوشته باشیم، دیتابیس در دسترس نباشه یا داده از نظر نوع با چیزی که انتظار داریم نخونه. برای تشخیص SQL Injection باید نشون بدیم ورودی غیرقابل اعتماد روی ساختار Query اثر گذاشته، نه اینکه صرفاً یک پیام خطا دیده‌ایم. توی گزارش هم بهتره علت تأییدشده رو از احتمال و حدس جدا بنویسیم.

اشتباه دوم: فکر کنیم نبودن خطا یعنی امنیت کامل

از اون طرف، پنهان بودن پیام خطا هم دلیل امنیت کامل نیست. برنامه ممکنه خطای داخلی رو به کاربر نشون نده، ولی همچنان Query رو اشتباه بسازه. WAF یا کد وضعیت ۴۰۳ هم گواهی امنیت نیست. مهم اینه که مسیرهای ساخت پرس‌وجو چطور پیاده‌سازی شدن و شواهد واقعی آزمون چی می‌گن.

اشتباه سوم: فقط صفحه ورود رو بررسی کنیم

حواست نباشه فقط فرم Login رو نگاه کنی. جست‌وجو، API، فیلتر گزارش و حتی داده‌ای که دیروز ذخیره شده ممکنه امروز وارد SQL بشه. برای بررسی درست، ببین اطلاعات از کجا میان و در چه نقطه‌ای به Query تبدیل می‌شن. خیلی از ضعف‌های جدی اتفاقاً در قابلیت‌های ظاهراً ساده و کم‌توجه اتفاق می‌افتن.

اشتباه چهارم: چند کاراکتر رو ببندیم و خیال‌مون راحت بشه

گاهی برای مقابله با تزریق SQL یک لیست از کاراکترهای ممنوع می‌سازن و فکر می‌کنن مسئله حل شده. اما این کار ممکنه جست‌وجوی سالم کاربران رو خراب کنه و باز هم بعضی شکل‌های ساخت دستور رو پوشش نده. راه مطمئن‌تر اینه که اساساً به داده اجازه تغییر دستور ندیم؛ یعنی پارامترگذاری درست، Allowlist برای اجزای ساختاری و بازبینی کد.

اشتباه پنجم: اجرای ابزار رو با تحلیل فنی اشتباه بگیریم

اسم Burp Suite یا sqlmap رو بلد بودن می‌تونه در مسیر تست نفوذ بهت کمک کنه، ولی متخصص شدن به تحلیل وابسته‌ست. اگه نتونی توضیح بدی چرا Query ناامنه، چرا روش پارامتری امن‌تره و نتیجه تا چه حد ثابت شده، اجرای ابزار به‌تنهایی کمکت نمی‌کنه. بهتره اول منطق کد رو بفهمی و بعد در آزمایشگاه مجاز سراغ ابزار بری.

۳ تمرین فکری: آیا واقعاً SQL Injection پیدا کردیم؟

تمرین اول: نتیجه عجیب در جست‌وجو

فرض کن برای یک ژانر ناموجود جست‌وجو می‌کنی، اما چند بازی برمی‌گرده. این یعنی SQL Injection پیدا کردی؟ نه؛ ممکنه فیلتر برنامه اشتباه نوشته شده باشه، کش جواب قدیمی داده باشه یا وضعیت داده درست نباشه. تا وقتی نحوه ساخت SQL و اثر واقعی ورودی مشخص نشده، فقط یک رفتار غیرمنتظره داریم.

تمرین دوم: Query امنه ولی اطلاعات بقیه دیده می‌شه

حالا فرض کن همه Queryها پارامتری شدن، ولی یک عضو هنوز رزرو فرد دیگه‌ای رو می‌بینه. این بار باید دنبال تزریق SQL بگردیم؟ لزوماً نه؛ شاید سرور مجوز مالکیت رو بررسی نکرده. پرس‌وجوی امن جلوی تغییر ساختار دستور رو می‌گیره، اما خودش تصمیم نمی‌گیره کدوم کاربر حق دیدن کدوم داده رو داره.

تمرین سوم: حساب دیتابیس مجوزهای زیادی داره

یه بخش جست‌وجوی عمومی هم داریم که با حساب دیتابیسی بسیار پرقدرت وصل می‌شه. این وضعیت باید اصلاح بشه، ولی آیا خودش اثبات SQL Injection هست؟ نه. سطح دسترسی زیاد، خطر بالقوه رو بالا می‌بره و اصل کمترین مجوز رو نقض می‌کنه، اما با وجودش نمی‌تونیم خودکار ادعا کنیم ورودی‌ها دستور SQL رو تغییر می‌دن.

اگه توی این سه تمرین تونستی به‌جای حدس فوری، نوع شاهد لازم رو مشخص کنی، داری درست فکر می‌کنی. توی امنیت وب همیشه قرار نیست در پایان هر بررسی یک حمله پیدا بشه؛ گاهی نتیجه حرفه‌ای اینه که بگیم «این بخش هنوز اثبات نشده». همین مرزبندی اعتبار گزارش و آموزش رو بالا می‌بره.

جلوگیری از SQL Injection

بعد از SQL Injection کدوم درس امنیت وب رو بخونیم؟

برای ادامه یادگیری، سه مفهوم رو خوب جا بنداز: ورودی غیرقابل اعتماد، ساختار دستور SQL و پارامترگذاری. بعد سراغ نمونه‌های پیچیده‌تر برو، ترجیحاً داخل آزمایشگاه‌های آموزشی مجاز مثل بخش SQL Injection در Web Security Academy. برای اینکه جای این درس رو در کل پروژه امنیت ببینی، مراحل تست نفوذ و ابزارهای تست نفوذ هم مکمل‌های خوبی‌ان.

اگه دوست داری این مباحث رو مرحله‌به‌مرحله و با تمرین جدی‌تر یاد بگیری، می‌تونی صفحه پکیج متخصص تست نفوذ نابغه‌ها رو ببینی و سرفصل‌ها و نمونه تدریس رو بررسی کنی. قرار نیست با خوندن یک مقاله همه بخش‌های امنیت وب رو بلد بشی؛ هدف اینه که ریشه SQL Injection رو بفهمی و وقتی رفتی سراغ تمرین عملی، بدونی دقیقاً دنبال چی هستی.

سؤالات متداول درباره SQL Injection

SQL Injection چیست به زبان ساده؟

ببین، وقتی می‌گیم SQL Injection، یعنی داده‌ای که کاربر فرستاده به‌خاطر ساخته شدن ناامن پرس‌وجو تونسته روی ساختار SQL اثر بذاره. در روش امن، مقدار جدا ارسال می‌شه و اجازه نداره نقش دستور رو بازی کنه.

آیا SQL Injection همان هک دیتابیس است؟

نه. تزریق SQL یک نوع آسیب‌پذیریه و نتیجه اون به Query، مجوز حساب دیتابیس و کنترل‌های برنامه بستگی داره. دیدن یک نتیجه غیرعادی به معنی تصاحب کل دیتابیس نیست.

آیا بدون دانستن SQL می‌شود SQL Injection را یاد گرفت؟

برای فهم اصل ماجرا لازم نیست متخصص دیتابیس باشی، ولی بهتره SELECT، WHERE و تفاوت مقدار با دستور رو بلد باشی. همین‌ها باعث می‌شن مثال‌ها رو واقعاً بفهمی، نه اینکه فقط حفظشون کنی.

آیا هر سایتی که فرم جست‌وجو دارد آسیب‌پذیر است؟

نه. فرم جست‌وجو به‌خودی‌خود ایرادی نداره؛ مهم اینه که سمت سرور چطور Query ساخته می‌شه. با یک پرس‌وجوی پارامتری درست، داده ورودی نمی‌تونه ساختار SQL رو تغییر بده.

فرق SQL Injection و XSS چیست؟

در تزریق SQL مشکل معمولاً در مرز داده و دستور دیتابیسه، ولی در XSS داده نامطمئن ممکنه هنگام نمایش به کد قابل اجرای مرورگر تبدیل بشه. روش تشخیص و دفاع این دو با هم فرق داره.

آیا WAF یا افزونه امنیتی جلوی همه حملات SQL Injection را می‌گیرد؟

نه، هیچ WAFی جای اصلاح کد رو نمی‌گیره. برای جلوگیری از SQL Injection باید از API پارامتری درست استفاده کنیم، اجزای ساختاری رو کنترل کنیم و سطح دسترسی دیتابیس رو هم محدود نگه داریم.

آیا WordPress و WooCommerce به‌صورت پیش‌فرض SQL Injection دارند؟

نه. وردپرس و ووکامرس صرفاً به خاطر اسمشون SQL Injection ندارن. مشکل ممکنه در افزونه، قالب یا Query سفارشی اشتباه ایجاد بشه و برای اثباتش باید نسخه و کد یا شواهد فنی بررسی بشه.

برای تمرین SQL Injection از کجا شروع کنیم؟

بهترین شروع همین آزمایش SQLite داخل مقاله‌ست؛ کاملاً آفلاینه. بعدش می‌تونی آزمایشگاه‌های رسمی و مجاز رو امتحان کنی. روی سایت واقعی بدون مجوز روشن، ورودی آزمایشی نفرست.

منابع معتبر برای بررسی دقیق‌تر

برای اینکه بعد از این درس سراغ منابع دقیق‌تر بری، راهنمای پیشگیری OWASP، راهنمای آزمون WSTG، مستندات sqlite3 پایتون و مستندات وردپرس برای wpdb::prepare رو پیشنهاد می‌کنم. برای دسته‌بندی رسمی این ضعف هم CWE-89 از MITRE مفیده. سناریوی باشگاه و کد آموزشی این درس مستقل و ساختگی هستن.

جمع‌بندی: اول کد رو بفهم، بعد درباره هک سایت حرف بزن

حالا اگه کسی ازت پرسید SQL Injection چیست، می‌تونی به‌جای یه تعریف کلی درباره هک دیتابیس، مسیر خطا رو توضیح بدی: ورودی کاربر وارد ساختار Query شده و رفتار دستور رو تغییر داده. توی آزمایش خودمون دیدیم چرا جست‌وجوی ناامن با یک ورودی غیرعادی نتیجه اشتباه می‌ده و چرا نسخه پارامتری همون ورودی رو فقط مقدار می‌بینه. همین فهم دقیق، پایه کدنویسی امن و گزارش درست آسیب‌پذیریه.

یه جمله رو از کل این درس نگه دار: کاربر باید بتونه مقدار بده، نه اینکه دستور دیتابیس رو بازنویسی کنه. برای جلوگیری از تزریق SQL این اصل رو در همه جا رعایت کن، از فرم جست‌وجوی ساده تا Queryهای اختصاصی وردپرس. بعد هم فراموش نکن که کنترل دسترسی، حداقل مجوز و آزمایش‌های واقعیِ مجاز لازم‌ان؛ هیچ کدوم از این‌ها به‌تنهایی معادل امن بودن کل سایت نیست.

می‌خواهی عمیق‌تر جلو بروی؟

این درس فقط یک بخش از مسیر کامل است

اگر می‌خواهی مباحث را به‌ترتیب، همراه تمرین و پروژه دنبال کنی، مسیر کامل دوره را ببین.

مشاهده پکیج تست نفوذ