SQL Injection یا تزریق SQL یعنی مهاجم میتواند ساختار کوئری پایگاهدادهی شما را تغییر دهد، چون ورودی او بهجای «داده» بهعنوان «بخشی از دستور» تفسیر شده است. نتیجه در بدترین حالت خواندن کل پایگاهداده، تغییر داده، دور زدن احراز هویت و در برخی پیکربندیها رسیدن به اجرای دستور روی سرور است. شناسهی استاندارد آن CWE-89 است، بیش از ۱۴٬۰۰۰ CVE ذیل آن ثبت شده، در فهرست ۲۰۲۵ خطرناکترین ضعفهای MITRE رتبهی دوم را دارد و در OWASP Top 10:2025 زیر دستهی A05 تزریق مینشیند. این مقاله انواع آن را با مثال توضیح میدهد و بعد به سراغ دو باور غلطی میرود که در محتوای فارسی مدام تکرار میشوند.
در یک نگاه
- علت ریشهای همیشه یکی است: آمیختن کد و داده. راهحل، جدا کردن آن دو است — نه پاکسازی ورودی.
- انواع اصلی: درونباند (UNION و خطامحور)، بلایند بولی، بلایند زمانی، خارجباند و مرتبهدوم.
- کوئری پارامتری راهحل درست است اما همهجا کافی نیست: شناسهها (نام جدول و ستون و
ORDER BY) پارامتریشدنی نیستند و به فهرست سفید نیاز دارند. - رویههای ذخیرهشدهای که داخل خودشان رشته میچسبانند، راههای فرار ORM، تزریق مرتبهدوم و NoSQL هم بیرون از پوشش پارامتریسازیاند.
- PHP با
PDO::ATTR_EMULATE_PREPARES = trueدر واقع در سمت درایور رشته میچسباند؛ این تنظیم بههمراه ناهمخوانی charset، سابقهی دور زدن واقعی دارد.
تزریق SQL چیست و چرا رخ میدهد؟
برنامهی شما برای گرفتن یک محصول از پایگاهداده، یک رشتهی SQL میسازد. اگر این رشته با چسباندن ورودی کاربر ساخته شود، مهاجم میتواند کاراکترهایی بفرستد که معنای دستور را عوض کنند. این کل ماجراست.
-- الگوی آسیبپذیر (رشته با هم چسبانده شده)
"SELECT * FROM products WHERE id = " + request.id
-- ورودی عادی: 42
-- ورودی مهاجم: 42 OR 1=1در حالت دوم، کوئری اجراشده بهجای یک محصول، همهی محصولها را برمیگرداند. همان الگو در فرم ورود، نتیجهی خطرناکتری دارد: مقدار کلاسیک ' OR '1'='1 در فیلد نام کاربری میتواند شرط بررسی رمز عبور را همیشهدرست کند و احراز هویت را دور بزند.
مشکل «کاراکتر تککوتیشن» نیست. مشکل این است که پایگاهداده هیچ راهی ندارد بفهمد کدام بخش از رشتهای که دریافت کرده «دستورِ برنامهنویس» و کدام بخش «ورودیِ کاربر» بوده است. هر تلاشی برای جبران این موضوع با فیلتر کردن کاراکترها، جنگی است در سمت اشتباه مسئله. توضیح مکانیزم در سطح تجزیه و اجرای کوئری، در دانشنامهی تزریق SQL آمده است.
چون هرگز فقط یک مسیر کوئری در برنامه وجود ندارد. تیمها لایهی دادهی اصلی را با ORM و کوئری پارامتری مینویسند، اما گزارشساز، اسکریپت مهاجرت داده، اندپوینت جستوجوی پیشرفته، صفحهی فیلتر پویا و آن یک کوئری «موقت» که سه سال پیش نوشته شد، همه با رشتهچسبانی ساخته شدهاند. یافتههای واقعی تست نفوذ تقریباً همیشه در همین حاشیهها هستند.
انواع تزریق SQL با مثال
۱) درونباند (In-band) — نتیجه در همان پاسخ برمیگردد
سادهترین و سریعترین حالت برای مهاجم، چون داده در همان صفحه دیده میشود. دو زیرشاخه دارد:
مبتنی بر UNION: مهاجم یک SELECT دوم را به نتیجهی کوئری اصلی میچسباند. پیششرطش این است که تعداد و نوع ستونها همخوان باشد، و به همین دلیل گام اول همیشه شمردن ستونهاست:
-- شمارش ستونها با ORDER BY
' ORDER BY 1-- → خطا ندارد
' ORDER BY 4-- → خطا: ستون چهارم وجود ندارد
-- سپس اتصال یک SELECT دوم با تعداد ستون همخوان
' UNION SELECT NULL, version()--خطامحور (Error-based): برنامه پیام خطای پایگاهداده را نمایش میدهد و مهاجم داده را داخل متن خطا بیرون میکشد. این مورد نشان میدهد چرا نمایش خطای پرجزئیات در محیط عملیاتی، خودش یک یافته است — موضوعی که در OWASP Top 10:2025 زیر دستهی A10 مدیریت نادرست شرایط استثنایی هم به آن اشاره شده.
۲) بلایند بولی (Boolean-based blind)
هیچ دادهای برنمیگردد و خطایی هم دیده نمیشود، اما پاسخ برنامه برای شرط درست و غلط متفاوت است: صفحهی «محصول یافت شد» در برابر «یافت نشد». مهاجم با پرسیدن هزاران سؤال بله/خیر، داده را یک بیت در هر بار استخراج میکند:
-- شرط درست: صفحه عادی برمیگردد
' AND (SELECT SUBSTRING(name,1,1) FROM users LIMIT 1)='a'--۳) بلایند زمانی (Time-based blind)
وقتی پاسخ برای درست و غلط هیچ تفاوتی ندارد، مهاجم زمان را به کانال اطلاعاتی تبدیل میکند: اگر شرط درست بود، پایگاهداده چند ثانیه تأخیر کند. کندترین روش است اما در برابر بسیاری از دفاعهای سطحی مقاوم است، و در تست نفوذ اغلب تنها راه اثبات یافته در اندپوینتهایی است که هیچ خروجی قابل مشاهدهای ندارند.
۴) خارجباند (Out-of-band)
پایگاهداده وادار میشود خودش یک اتصال شبکهای بیرونی برقرار کند — معمولاً یک درخواست DNS یا HTTP — و داده را داخل نام دامنه بگذارد. این روش وقتی بهکار میآید که پاسخ HTTP هیچ سیگنالی ندارد و کانال زمانی هم پرنویز است. کشف آن نیازمند یک سرویس شنوندهی بیرونی است.
۵) مرتبهدوم (Second-order)
خطرناکترین نوع از منظر «پیدا نشدن». ورودی مهاجم در گام اول با کوئری پارامتری و کاملاً ایمن ذخیره میشود؛ اما بعداً یک بخش دیگر از سیستم — گزارش شبانه، پنل مدیریت، فرایند خروجیگیری — همان مقدار را از پایگاهداده میخواند و این بار با رشتهچسبانی در کوئری میگذارد. نه اسکنر خودکار این را میبیند و نه تست تکپارامتری؛ چون بین ورود بدنه و اجرای آن، هم زمان فاصله است و هم مسیر.
| نوع | سیگنال تشخیص | سرعت استخراج | احتمال کشف با اسکنر |
|---|---|---|---|
| UNION | داده در پاسخ | بسیار سریع | بالا |
| خطامحور | پیام خطای پایگاهداده | سریع | بالا |
| بلایند بولی | تفاوت محتوای پاسخ | کند | متوسط |
| بلایند زمانی | تفاوت زمان پاسخ | بسیار کند | متوسط |
| خارجباند | تعامل DNS/HTTP بیرونی | متوسط | پایین |
| مرتبهدوم | اجرا در مسیر و زمان دیگر | متغیر | بسیار پایین |
باور غلط اول: «کوئری پارامتری، تزریق SQL را غیرممکن میکند»
ابتدا انصاف: کوئری پارامتری (Prepared Statement) راهحل درست است و دلیلش ساختاری است. ساختار کوئری جدا از داده به پایگاهداده فرستاده و تجزیه و برنامهریزی میشود؛ مقادیر پارامترها بعد از آن مقیّد میشوند و دیگر هرگز بهعنوان SQL تجزیه نمیشوند. به همین دلیل هیچ مقدار کوتیشن، کامنت یا عملگری در داده نمیتواند معنای کوئری را عوض کند. این با فیلتر کردن تفاوت ماهوی دارد.
«از Prepared Statement استفاده میکنیم، پس تزریق SQL منتفی است.» این جمله برای بخش مقادیر درست است و برای بخشهای دیگر برنامه نادرست. نُه حالت زیر بیرون از پوشش پارامتریسازیاند و هر کدام در تست نفوذ واقعی دیده میشوند.
۱) شناسهها را نمیتوان پارامتری کرد
نام جدول، نام ستون و نام اسکیما در SQL استاندارد قابل مقیّدسازی نیستند. یعنی این الگوهای بسیار رایج بیرون از حفاظتاند: ORDER BY {ستون}، جهت مرتبسازی، انتخاب پویای جدول، و SELECT {ستون}. راه درست، نگاشت فهرست سفید در سمت سرور است: ورودی کاربر یک کلید است که به نام ستون ثابت و از پیش تعیینشده نگاشت میشود، نه رشتهای که فرار داده میشود.
# الگوی درست: نگاشت فهرست سفید برای شناسه
SORT = {"price": "unit_price", "new": "created_at"}
col = SORT.get(user_sort, "created_at") # مقدار نامعتبر → پیشفرض
sql = f"SELECT * FROM products ORDER BY {col} LIMIT ?"۲) جهت مرتبسازی و LIMIT/OFFSET
در چند درایور و چند پایگاهداده، ASC/DESC و مقادیر LIMIT و OFFSET قابل مقیّدسازی نیستند. برای اینها هم فهرست سفید یا تبدیل صریح به عدد صحیح لازم است.
۳) قطعههای پویای کوئری
سازندههای شرط WHERE، فهرستهای IN (...) که با حلقه و join رشتهای ساخته میشوند، و DSLهای جستوجوی پیشرفته که در نهایت به SQL ترجمه میشوند. اینجا پارامتریسازی برای مقادیر انجام میشود اما ساختار همچنان از ورودی کاربر ساخته میشود.
۴) رویههای ذخیرهشدهای که خودشان رشته میچسبانند
فراخوانی پارامتری یک رویه، اگر خودِ رویه داخلش SQL پویا بسازد، هیچ محافظتی ایجاد نمیکند. باور «رویهی ذخیرهشده امن است» یکی از پایدارترین اشتباهها در پروژههای SQL Server و Oracle است.
۵) وایلدکاردهای LIKE
پارامتریسازی جلوی تزریق را میگیرد اما کاراکترهای % و _ را بیاثر نمیکند. مهاجم با فرستادن % میتواند دامنهی تطبیق را گسترش دهد یا با الگوهای پرهزینه، پیمایش کامل جدول و در نتیجه اختلال در دسترسپذیری ایجاد کند. این کاراکترها باید جداگانه فرار داده شوند.
۶) راههای فرار ORM
ORM آسیبپذیری را حذف نمیکند، آن را جابهجا میکند. هر ORM یک درِ خروج به SQL خام دارد: Model.raw()، session.execute(text(...))، چسباندن رشته در HQL هایبرنیت، .extra() و RawSQL در جنگو، و sequelize.query با درج مقدار. در بازبینی کد، جستوجوی همین نامها اولین کاری است که انجام میدهیم.
۷) تزریق مرتبهدوم
مقدار در نوشتن پارامتری بود و در خواندن چسبانده شد. توضیحش در بخش انواع آمد؛ نکته این است که «ما همهجا پارامتری مینویسیم» تا وقتی مسیر خواندن هم بازبینی نشده، ادعای کاملی نیست.
۸) آمادهسازی تقلیدی در درایور
در PHP، تنظیم PDO::ATTR_EMULATE_PREPARES = true — که سابقاً پیشفرض MySQL بود — باعث میشود درایور خودش مقدار را در رشته درج کند، نه اینکه آمادهسازی واقعی در سمت سرور انجام شود. این حالت بههمراه ناهمخوانی charset چندبایتی، سابقهی دور زدن واقعی دارد. تنظیم درست: emulate_prepares را خاموش کنید و charset اتصال را صریح و درست تعیین کنید.
۹) NoSQL
در پایگاهدادههای سندگرا، «پارامتریسازی» معنای مشخصی ندارد. مشکل، تزریق عملگر است: مقداری که باید رشته باشد، بهصورت یک آبجکت مثل {"$ne": null} یا {"$gt": ""} فرستاده میشود و منطق شرط را عوض میکند؛ و ارزیابی جاوااسکریپت با $where کلاس خطر جداگانهای است. راهحل اینجا اعتبارسنجی نوع است: اگر انتظار رشته دارید، آبجکت را رد کنید.
باور غلط دوم: «ورودی را پاکسازی و فرار میدهیم، پس امن است»
«همهی ورودیها را با تابع فرار پایگاهداده و حذف کلمات کلیدی مثل UNION و SELECT پاک میکنیم.» این رویکرد ذاتاً شکننده است. دفاع درست، جداسازی کد از داده است؛ فیلتر کردن ورودی حداکثر یک لایهی مکمل است و هرگز کنترل اصلی نیست.
سه دلیل فنی روشن:
- سابقهی دور زدن مبتنی بر charset. توابع فرار باید بدانند اتصال با چه کدگذاری کاراکتری کار میکند. ناهمخوانی بین charset اعلامشده و charset واقعی اتصال، در مجموعههای چندبایتی به دور زدن قابل بهرهبرداری منتهی شده است. این یک کلاس مشکل تاریخی و مستند است، نه یک احتمال نظری.
- فهرست سیاه کلمات کلیدی همیشه ناقص است. تغییر حروف بزرگ و کوچک، کامنت درونکوئری، کدگذاری، فاصلههای جایگزین و توابع معادل، همه راههای شناختهشدهی عبور از فیلترهای رشتهایاند. مدافع باید همهی حالتها را بشناسد؛ مهاجم فقط یکی را.
- حذف ورودی، دادهی درست را هم خراب میکند. نام خانوادگی با آپاستروف، متن حاوی خط تیرهی دوگانه، و رمز عبوری با کاراکتر خاص، همه قربانی پاکسازیِ کورکورانه میشوند. این هم به تجربهی کاربر آسیب میزند و هم توسعهدهنده را به «استثنا گذاشتن» تشویق میکند — و استثناها همان جایی هستند که آسیبپذیری زنده میماند.
اعتبارسنجی ورودی جای خودش را دارد و OWASP هم آن را توصیه میکند — اما به شکل فهرست سفید و در سمت سرور: این فیلد باید عدد صحیح باشد، این یکی باید یکی از این پنج مقدار باشد، این یکی الگوی کد پستی را داشته باشد. این کار سطح حمله را کم میکند، ولی جانشین پارامتریسازی نیست. همانطور که در دانشنامهی WAF توضیح داده شده، WAF هم راهکار رفع نیست: یک لایهی خریدن زمان است که با کدگذاری چندلایه، تغییر Content-Type، بدنههای بزرگتر از حد بازرسی و بهویژه پیدا کردن IP اصلی سرور دور زده میشود.
اثر واقعی: از خواندن یک جدول تا اجرای دستور روی سرور
شدت یک تزریق SQL را باید بر پایهی سه چیز سنجید: چه دادهای در دسترس است، مجوزهای کاربر پایگاهداده چیست، و آیا نوشتن هم ممکن است یا فقط خواندن.
- خواندن داده: جدول کاربران با هش رمز عبور، سفارشها، اطلاعات هویتی و اسناد. در بسیاری از سامانههای ایرانی این یعنی دادهی مشمول تعهدات قانونی حفاظت از دادهی مشتری.
- دور زدن احراز هویت: ورود به حساب مدیر بدون دانستن رمز.
- تغییر و حذف داده: تغییر مبلغ سفارش، ارتقای نقش خود به مدیر، حذف لاگها.
- ماندگاری: ساخت کاربر ادمین جدید که پس از رفع باگ هم باقی میماند. به همین دلیل در بازیابی سایت هکشده، صرفِ بستن آسیبپذیری کافی نیست.
- رسیدن به سیستمعامل: بسته به پایگاهداده و مجوزها — نوشتن فایل در ریشهی وب، فراخوانی رویههای سیستمی، یا بارگذاری تابع کاربر. این مسیر همیشه باز نیست، اما وقتی کاربر پایگاهداده مجوز بیش از حد دارد، احتمالش جدی است.
و همین ما را به مهمترین کنترل مکمل میرساند: کمینهسازی مجوز کاربر پایگاهداده. کاربری که برنامه با آن وصل میشود نباید root یا sa باشد، نباید مجوز نوشتن فایل داشته باشد، نباید بتواند ساختار جدول را تغییر دهد، و نباید به پایگاهدادههای دیگر همان نمونه دسترسی داشته باشد. این کار باگ را رفع نمیکند اما تفاوت بین «افشای یک جدول» و «در اختیار گرفتن سرور» را میسازد.
تزریق SQL چگونه در تست نفوذ کشف میشود؟
در راهنمای آزمون امنیت وب OWASP (نسخهی پایدار WSTG v4.2)، آزمون تزریق SQL زیر بخش اعتبارسنجی ورودی قرار دارد و هشت زیرآزمون اختصاصی بر اساس پایگاهداده دارد: Oracle، MySQL، SQL Server، PostgreSQL، MS Access، NoSQL، ORM و سمتکلاینت. دلیلش این است که نشانههای تشخیص و نحو هر پایگاهداده متفاوت است.
روال عملی:
- شناسایی نقاط ورود. نه فقط پارامترهای URL: بدنهی JSON و XML، کوکیها، هدرهای سفارشی، فیلدهای فایل آپلودی، و مقادیری که از سرویسهای دیگر میآیند.
- تشخیص تفاوت رفتار. ابتدا با ورودیهای بیخطر بررسی میکنیم آیا کاراکترهای معنادار SQL باعث تغییر پاسخ میشوند: یک کوتیشن، یک عملگر ریاضی معادل، یک شرط همیشهدرست و یک شرط همیشهغلط.
- مشخص کردن نوع و پایگاهداده. تفاوت نحو توابع رشته و تأخیر، بهترین اثر انگشت است.
- اثبات اثر با کمترین تهاجم. در یک تست حرفهای، هدف اثبات وجود آسیبپذیری است، نه استخراج کل پایگاهداده. استخراج نمونهای محدود و توافقشده، در چارچوب سند دامنهی کار انجام میشود.
- بررسی مرتبهدوم. مقدار نشاندار را در فرم ورودی ثبت میکنیم و بعد همهی مسیرهای نمایش و گزارش را میگردیم.
ابزار تخصصی این حوزه sqlmap است که برای تشخیص و استخراج خودکار بسیار قوی است — اما دو نکته: اول اینکه اجرای آن روی سامانهای که مجوز کتبی ندارید، تخلف است؛ دوم اینکه sqlmap موارد مرتبهدوم و منطقی را از دست میدهد و خروجیاش بدون تحلیل انسانی هم مثبت کاذب دارد و هم منفی کاذب. کنار آن، پروکسی رهگیر مثل Burp Suite و بازبینی کد منبع پوشش را کامل میکنند. جزئیات بیشتر در راهنمای تست نفوذ وب آمده است.
راهکار رفع، به ترتیب اثربخشی
- کوئری پارامتری برای همهی مقادیر. بدون استثنا، در همهی مسیرها — شامل اسکریپتهای عملیاتی، گزارشسازها و کارهای زمانبندیشده.
- فهرست سفید برای شناسهها. نام ستون، نام جدول، جهت مرتبسازی: نگاشت از کلید کاربر به مقدار ثابت. هرگز فرار دادن، هرگز درج مستقیم.
- اعتبارسنجی نوع و دامنه در سمت سرور. عدد باید عدد باشد؛ در APIهای NoSQL، ورودیهایی که آبجکتاند و باید رشته باشند، رد شوند.
- حذف SQL پویا از رویههای ذخیرهشده یا پارامتری کردن آن با
sp_executesqlو معادلهایش. - ممیزی راههای فرار ORM. یک قاعده در ابزار تحلیل استاتیک که همهی فراخوانیهای SQL خام را علامت بزند و آنها را نیازمند بازبینی کند.
- کمینهسازی مجوز کاربر پایگاهداده و جداسازی کاربر خواندن از کاربر نوشتن، جایی که ممکن است.
- خطاهای عمومی در محیط عملیاتی. پیام خطای پایگاهداده هرگز نباید به کاربر برسد؛ جزئیات به لاگ سمت سرور برود.
- SAST و DAST در خط لولهی CI/CD — توصیهی صریح OWASP برای دستهی A05:2025.
- WAF فقط بهعنوان لایهی موقت. برای خریدن زمان تا استقرار اصلاح، بله؛ بهعنوان راهکار رفع در گزارش، هرگز.
در سطح فرایند، دو کار بیشترین بازده را دارد: آموزش الگوی درست به تیم توسعه — چون این آسیبپذیری تقریباً همیشه یک الگوی کدنویسی تکرارشونده است، و همان چیزی که در دورهی سازمانی توسعهی امن هدف قرار میگیرد — و آزمون مجدد پس از رفع. برای ارزیابی وضعیت فعلی، تست نفوذ وب هر نقطهی ورود را با اثبات اثر و راهکار مشخص بررسی میکند؛ اگر میخواهید ابتدا یک تصویر کلی داشته باشید، بررسی امنیت سایت و چکلیست امنیتی نقطهی شروع مناسبی هستند.
پرسشهای متداول
تزریق SQL چیست و چه خطری دارد؟
تزریق SQL یعنی مهاجم میتواند با فرستادن ورودی خاص، ساختار کوئری پایگاهداده را تغییر دهد. اثر آن از خواندن کل پایگاهداده و دور زدن احراز هویت شروع میشود و بسته به مجوزهای کاربر پایگاهداده میتواند به تغییر داده و حتی اجرای دستور روی سرور برسد. شناسهی استاندارد آن CWE-89 است و در OWASP Top 10:2025 زیر دستهی A05 تزریق قرار دارد.
آیا کوئری پارامتری جلوی تزریق SQL را میگیرد؟
برای مقادیر بله، و این راهحل درست است. اما شناسهها (نام جدول و ستون، ORDER BY) پارامتریشدنی نیستند و به فهرست سفید نیاز دارند. همچنین رویههای ذخیرهشدهای که داخل خودشان رشته میچسبانند، راههای فرار ORM، تزریق مرتبهدوم، وایلدکاردهای LIKE، NoSQL و PDO با EMULATE_PREPARES=true بیرون از این پوششاند.
تفاوت تزریق SQL بلایند و معمولی چیست؟
در حالت معمولی (درونباند) داده در همان پاسخ HTTP دیده میشود — با UNION یا داخل پیام خطا. در حالت بلایند هیچ دادهای برنمیگردد و مهاجم از سیگنالهای غیرمستقیم استفاده میکند: تفاوت محتوای پاسخ برای شرط درست و غلط (بلایند بولی) یا تفاوت زمان پاسخ (بلایند زمانی). بلایند کندتر است ولی به همان اندازه خطرناک.
آیا استفاده از ORM مثل Eloquent یا Hibernate امنیت را تضمین میکند؟
خیر. ORM بهطور پیشفرض کوئری پارامتری تولید میکند و همین بخش بزرگی از خطر را حذف میکند، اما هر ORM یک راه فرار به SQL خام دارد و همانجا آسیبپذیری برمیگردد. علاوه بر این، ORDER BY پویا و نام ستون قابل انتخاب توسط کاربر در ORM هم مشکلساز میماند. ORM ریسک را جابهجا میکند، حذف نمیکند.
آیا WAF میتواند جلوی تزریق SQL را بگیرد؟
WAF میتواند حجم زیادی از ترافیک اسکن خودکار را مسدود کند و برای خریدن زمان تا استقرار اصلاح ارزشمند است، اما راهکار رفع نیست. با کدگذاری چندلایه، تغییر Content-Type، بدنهی بزرگتر از حد بازرسی و بهویژه پیدا کردن IP اصلی سرور دور زده میشود. در گزارش تست نفوذ، «نصب WAF» هرگز نباید بهعنوان رفع یک آسیبپذیری کد ثبت شود.
چطور بفهمم سایتم تزریق SQL دارد؟
ترکیب سه کار: بررسی همهی نقاط ورود با ورودیهایی که رفتار متفاوت ایجاد میکنند (نه فقط پارامترهای URL)، بازبینی کد برای الگوی رشتهچسبانی و فراخوانیهای SQL خام، و آزمون مسیرهای مرتبهدوم که مقدار ذخیرهشده بعداً در کوئری دیگری استفاده میشود. اسکن خودکار بهتنهایی موارد مرتبهدوم و منطقی را از دست میدهد.
