تزریق SQL چیست و چگونه رخ میدهد؟
تزریق SQL (SQL Injection) که بهاختصار SQLi خوانده میشود، زمانی رخ میدهد که برنامه ورودی کاربر را بدون جداسازی از دستور، داخل یک کوئری SQL میچسباند؛ در نتیجه مفسر پایگاهداده بخشی از ورودی را بهعنوان دستور اجرا میکند نه داده. پیامد آن میتواند دور زدن احراز هویت، خواندن یا تغییر کل داده، و در شرایطی رسیدن به اجرای فرمان روی سیستمعامل باشد (مثلاً از طریق xp_cmdshell در SQL Server یا INTO OUTFILE در MySQL).
ریشهی مشکل، آمیختن کد و داده در یک رشته است. وقتی مقدار id=1 به id=1 OR 1=1 تبدیل میشود، ساختار منطقی کوئری تغییر میکند. به همین دلیل راهحل بنیادی، جداسازی ساختاری کد از داده است، نه فیلتر کردن کاراکترهای «بد».
در مقیاس، CWE-89 با بیش از ۱۴٬۰۰۰ CVE و رتبهی دوم فهرست ۲۵گانهی CWE سال ۲۰۲۵، یکی از پرتکرارترین یافتههای هر تست نفوذ است.
انواع تزریق SQL
بر پایهی نحوهی بازگشت داده به مهاجم، پنج خانواده متمایز میشوند:
- درونباند (In-band) / UNION: نتیجه مستقیماً در پاسخ برنامه دیده میشود. UNION-based دادهی جداول دیگر را به خروجی میچسباند و Error-based از پیام خطای پایگاهداده برای استخراج بهره میبرد.
- کور بولی (Blind Boolean): پاسخ خروجی داده را نشان نمیدهد، اما رفتار برنامه بین شرط درست و نادرست فرق میکند؛ مهاجم بیتبهبیت داده را استنتاج میکند.
- کور مبتنی بر زمان (Blind Time-based): وقتی حتی تفاوت رفتاری هم دیده نمیشود، با تابع تأخیر (مثل
SLEEP) پاسخِ درست/نادرست از روی زمان پاسخ استنتاج میشود. - خارجباند (Out-of-band): استخراج داده از کانالی جدا مثل درخواست DNS یا HTTP؛ کاربردی زمانی که کانال درونباند بسته است.
- مرتبهدوم (Second-order): بار در یک مرحله ذخیره میشود (و شاید در همان لحظه بیخطر باشد) و در کوئری دیگری بعداً بدون جداسازی اجرا میشود.
ابزار مرجع خودکارسازی، sqlmap است که هر پنج تکنیک را پوشش میدهد؛ اما تزریق مرتبهدوم نیازمند پیکربندی صریح (--second-url) است و در برابر NoSQL و اغلب ORMها کارایی ندارد.
چرا کوئری پارامتری همیشه کافی نیست؟
کوئری پارامتری (Prepared Statement) رفع درست است، چون ساختار کوئری جدا از داده به پایگاه ارسال و برنامهریزی میشود و مقادیر بعداً bind میشوند و هرگز دوباره بهعنوان SQL تفسیر نمیشوند. اما این پوشش، مطلق نیست.
«کوئری پارامتری تزریق SQL را غیرممکن میکند.» ناقص است. کوئری پارامتری این موارد را پوشش نمیدهد: (۱) شناسهها — نام جدول، نام ستون و عبارت ORDER BY قابل bind نیستند؛ (۲) رویههای ذخیرهشدهای که داخل خودشان رشته میچسبانند و SQL پویا اجرا میکنند؛ (۳) راههای فرار ORM مثل raw()، session.execute(text(...))، الحاق رشته در HQL و .extra() در Django؛ (۴) تزریق مرتبهدوم؛ (۵) NoSQL که پارامتریسازی در آن معنا ندارد (تزریق عملگر مانند {"$ne": null})؛ و (۶) PDO با ATTR_EMULATE_PREPARES=true که درج را در درایور انجام میدهد و همراه ناهماهنگی charset چندبایتی به دور زدن واقعی منجر شده است.
برای شناسهها راه درست، درج رشته یا escape نیست؛ بلکه یک فهرست سفید (allowlist) سمت سرور است که ورودی کاربر را به مجموعهی محدودی از نامهای مجاز نگاشت میکند.
«فرار دادن (escape) ورودی، تزریق SQL را میبندد.» فیلتر ورودی رفع نیست؛ جداسازی کد از داده رفع است. دفاعهای مبتنی بر escape سابقهی طولانی از دور زدن با charset و کاراکترهای چندبایتی دارند.
سناریوی واقعی و کشف در تست نفوذ
سناریوی متعارف: فرم ورود یک پنل مدیریت، نام کاربری را مستقیماً در کوئری میچسباند. مهاجم بهجای رمز، رشتهای میفرستد که شرط WHERE را همیشهدرست میکند و بدون دانستن رمز وارد میشود. در نمونهی جدیتر، یک پارامتر جستوجو به تزریق UNION آسیبپذیر است و کل جدول کاربران استخراج میشود.
روش کشف در تست نفوذ وب:
- شناسایی نقاط ورودی که به کوئری میرسند و تزریق یک کاراکتر شکننده (
') برای دیدن خطا یا تغییر رفتار. - تشخیص نوع: بازتاب مستقیم، تفاوت بولی، یا تأخیر زمانی.
- خودکارسازی با sqlmap از روی یک درخواست ذخیرهشده (
-r) — که مسیر حرفهای است، نه حدس زدن با-u. - مکمل با بازبینی کد منبع برای یافتن نقاط الحاق رشته که اسکن پویا از دست میدهد.
برای ارزیابی هدفمند این آسیب میتوانید از خدمات تست نفوذ وب استفاده کنید.
نمونهی فنی
شکل چند بار تشخیصی غیرمخرب (اثبات مفهوم):
-- تشخیص کور مبتنی بر زمان (MySQL)
id=1 AND SLEEP(5)-- -
-- استخراج با UNION
id=-1 UNION SELECT username, password FROM users-- -رفع درست برای مقدار، کوئری پارامتری است؛ و برای شناسه، فهرست سفید:
# شناسهها را نمیتوان bind کرد؛ نگاشت فهرست سفید لازم است
COLS = {"name": "name", "date": "created_at"}
col = COLS.get(user_input, "id") # پیشفرض امن
# مقدار با پارامتر bind میشود
cur.execute(f"SELECT * FROM t ORDER BY {col} LIMIT %s", (n,))
ارتباط با OWASP و CWE
تزریق SQL شناسهی CWE-89 را دارد و در OWASP Top 10:2025 زیر دستهی A05:2025 تزریق قرار میگیرد. در فهرست ۲۵گانهی CWE سال ۲۰۲۵ با امتیاز ۲۸٫۷۲ رتبهی دوم است. برای مرور دستهبندی ریسک، صفحهی OWASP و مقالهی تزریق SQL به زبان ساده را ببینید.
پرسشهای متداول
آیا کوئری پارامتری تزریق SQL را کاملاً حذف میکند؟
برای مقادیر، بله؛ اما شناسهها (نام جدول/ستون و ORDER BY)، رویههای ذخیرهشدهی الحاقی، راههای فرار ORM، تزریق مرتبهدوم، NoSQL و PDO با EMULATE_PREPARES=true را پوشش نمیدهد. برای شناسهها فهرست سفید لازم است.
تزریق SQL کور (Blind) چیست؟
حالتی که خروجی داده مستقیماً بازتاب نمیشود. در نوع بولی، تفاوت رفتار برنامه بین شرط درست و نادرست مبنای استنتاج است؛ در نوع زمانی، تأخیر پاسخ (با تابعی مثل SLEEP) پاسخ را مشخص میکند.
آیا فرار دادن (escape) ورودی برای جلوگیری از SQLi کافی است؟
خیر. فیلتر و escape ورودی رفع ریشهای نیست و سابقهی طولانی از دور زدن با charset چندبایتی دارد. راه درست جداسازی کد از داده با کوئری پارامتری و فهرست سفید برای شناسههاست.
آیا استفاده از ORM جلوی تزریق SQL را میگیرد؟
نه بهطور کامل. ORM ریسک را جابهجا میکند نه حذف. راههای فرار خام مثل Model.raw()، session.execute(text(...)) و الحاق رشته در HQL همچنان آسیبپذیرند.