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

SQL Injection یا به اختصار SQLi (تزریق SQL)، یک نوع حملهٔ سایبری است که در آن مهاجم با وارد کردن دستورات SQL در ورودی‌های برنامه، باعث اجرای کوئری‌های ناخواسته در پایگاه داده می‌شود. این حمله از سال‌ها پیش در فهرست ده آسیب‌پذیری خطرناک OWASP قرار داشته و همچنان یکی از رایج‌ترین دلایل نشت داده در وب است. مفهوم کلی آسیب‌پذیری را در آسیب‌پذیری وب چیست و چگونه شناسایی می‌شود؟ آورده‌ام؛ این مقاله، به لایهٔ عملیاتی و دفاعی این حملهٔ خاص می‌پردازد.

SQL Injection چیست و چه مسئله‌ای را حل نمی‌کند؟

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

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

SQL Injection یک حمله از جنس مرزهاست: مرز بین داده و کد. هر جا این مرز محو شود، مهاجم می‌تواند با همان داده، برنامه را کنترل کند.

چرا تزریق SQL خطرناک‌تر از سایر حملات است؟

در بین آسیب‌پذیری‌های وب، SQL Injection از چند جهت متمایز است:

مستقیم به دیتابیس می‌رسد

حمله‌های دیگر مثل XSS (Cross-Site Scripting) در مرورگر قربانی اجرا می‌شوند و محدود به همان کاربر هستند. SQL Injection اما مستقیم روی پایگاه داده اجرا می‌شود، جایی که قلب اطلاعات کسب‌وکار قرار دارد. یک حملهٔ موفق، می‌تواند همهٔ رکوردهای مشتریان، رمزهای عبور، اطلاعات پرداخت و اسرار تجاری را بیرون بکشد.

می‌تواند داده را نابود کند

حمله‌های دیگر معمولاً اطلاعات را می‌دزدند. SQL Injection می‌تواند دستور DELETE یا DROP TABLE را اجرا کند و کل پایگاه داده را از بین ببرد. در یکی از پروژه‌های پاکسازی که یادم می‌آید، مهاجم با یک خط، جدول سفارش‌های هفتهٔ گذشته را پاک کرده بود و بازیابی از بکاپ، هشت ساعت طول کشید.

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

در برخی پیکربندی‌های ناامن (مخصوصاً MySQL با دسترسی‌های زیاد)، مهاجم می‌تواند از طریق SQL Injection، فایل روی سرور بنویسد یا دستور سیستم اجرا کند. این لایه، مرز بین نفوذ به اپلیکیشن و تسخیر کامل سرور را محو می‌کند.

گاهی کاملاً ناشناس اجرا می‌شود

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

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

مکانیزم حمله در سه گام

برای دفاع مؤثر، باید بفهمید حمله دقیقاً چطور اتفاق می‌افتد:

گام اول: شناسایی ورودی آسیب‌پذیر

مهاجم به‌دنبال نقاطی است که ورودی کاربر مستقیم به کوئری SQL وصل می‌شود. سه علامت روشن وجود دارد: فرم‌های ورود و جستجو، پارامترهای URL مثل ?id=1، و فیلدهای API که پارامتر می‌گیرند. اکثر حملات موفق، از همین سه نقطه شروع می‌شوند.

گام دوم: تزریق دستور

مهاجم با ترفندهای مختلف، کد SQL را داخل ورودی می‌چسباند. مثلاً در فیلد ورود کاربر، به‌جای نام کاربری، رشته‌ای مثل adminx27 OR x271x27=x271 وارد می‌کند. اگر برنامه ناامن باشد، این رشته به کوئری‌ای تبدیل می‌شود که همیشه شرطش درست است و مهاجم بدون دانستن رمز، وارد می‌شود.

گام سوم: استخراج یا تخریب داده

بعد از این‌که کد SQL اجرا شد، مهاجم می‌تواند داده را استخراج کند (مثلاً با UNION SELECT)، داده را تغییر دهد (UPDATE)، یا آن را حذف کند (DELETE). بسته به دسترسی دیتابیس، امکان اجرای دستورهای سیستم هم وجود دارد.

نمونه‌ای واقعی: از ورودی ساده تا لو رفتن دیتابیس

برای این‌که تصویر روشن شود، یک کد PHP آسیب‌پذیر را کنار هم ببینیم:

$username = $_POST['username'];
$password = $_POST['password'];

$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($connection, $query);

if (mysqli_num_rows($result) > 0) {
    // ورود موفق
}

این کد در نگاه اول ساده و درست به نظر می‌رسد. اما اگر مهاجم در فیلد username رشتهٔ زیر را وارد کند:

adminx27 OR x271x27=x271x27 --

کوئری نهایی به این شکل تبدیل می‌شود:

SELECT * FROM users WHERE username = 'admin' OR '1'='1' -- ' AND password = '...'

شرط '1'='1' همیشه درست است، و علامت -- بقیهٔ کوئری را کامنت می‌کند. نتیجه: مهاجم بدون دانستن رمز، به حساب admin وارد می‌شود. همین تکنیک ساده، ریشهٔ نیمی از حمله‌های واقعی SQL Injection است.

حالا همان کد را با Prepared Statements بازنویسی کنیم:

$stmt = $connection->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();

در این نسخه، حتی اگر مهاجم همان رشتهٔ تزریقی را وارد کند، دیتابیس آن را به‌عنوان داده دیده و اجرا نمی‌کند. این تفاوت بنیادین، دلیل اصلی توصیهٔ اکید استفاده از Prepared Statements است. نحوهٔ پیاده‌سازی این تکنیک در PHP را در آموزش PDO در PHP باز کرده‌ام.

انواع SQL Injection که باید بشناسید

SQL Injection یک حمله تک‌شکلی نیست. چهار نوع اصلی وجود دارد:

نوعویژگیسطح خطر
In-band (کلاسیک)مهاجم نتیجه را مستقیم در پاسخ می‌بیندبحرانی
Blind Booleanنتیجه صرفاً به‌صورت بله یا خیر برمی‌گرددبالا
Blind Time-basedمهاجم از تأخیر پاسخ نتیجه می‌گیردبالا
Out-of-bandداده از کانال دیگری منتقل می‌شودمتوسط

در In-band یا کلاسیک، مهاجم مستقیم داده را از پاسخ برنامه می‌خواند؛ ساده‌ترین و خطرناک‌ترین نوع. در Blind، برنامه هیچ داده‌ای برنمی‌گرداند، ولی مهاجم با تکنیک‌های خاص (مثل پرسیدن سؤال‌های بله یا خیر یا اندازه‌گیری زمان پاسخ) داده را استخراج می‌کند. در Out-of-band، مهاجم برنامه را مجبور می‌کند داده را به سرور خودش بفرستد.

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

هفت لایه دفاعی عملی

دفاع در برابر SQL Injection، یک پروژهٔ چندلایه است. هفت لایه که در پروژه‌های خودم اجرا می‌کنم:

لایه اول: Prepared Statements

مؤثرترین دفاع. با استفاده از Prepared Statements، ورودی کاربر هیچ‌وقت به‌عنوان کد اجرا نمی‌شود، چون دیتابیس، ساختار کوئری را از داده جدا می‌کند. در PHP، این کار با PDO یا MySQLi انجام می‌شود. روش کامل در اتصال PHP به MySQL آمده است.

لایه دوم: اعتبارسنجی ورودی (Validation)

ورودی‌ها را قبل از هر چیز، از نظر شکل بررسی کنید. مثلاً اگر فیلد id فقط باید عدد باشد، بررسی کنید که عدد باشد. Validation جلوی ورودی‌های بی‌ربط را می‌گیرد. نکات کامل در اعتبارسنجی داده‌ها در کدنویسی وردپرس.

لایه سوم: پاک‌سازی داده (Sanitization)

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

لایه چهارم: اصل کمترین دسترسی دیتابیس

کاربر دیتابیس که برنامه از آن استفاده می‌کند، نباید دسترسی‌های مدیریتی داشته باشد. فقط SELECT، INSERT، UPDATE و DELETE روی جدول‌های مورد نیاز. اگر برنامه نباید DROP TABLE بزند، کاربر دیتابیس هم نباید آن دسترسی را داشته باشد. مقایسهٔ سطوح دسترسی در مدیریت کاربران و دسترسی‌های دیتابیس.

لایه پنجم: پنهان‌کردن پیام‌های خطا

پیام‌های خطای دیتابیس نباید به کاربر نهایی نمایش داده شوند، چون اطلاعات مفیدی به مهاجم می‌دهند. در محیط تولید، خطاها باید فقط در لاگ‌های سرور ذخیره شوند. راهنمای مدیریت خطا در مدیریت خطا در PHP.

لایه ششم: فایروال برنامهٔ وب (WAF)

یک WAF (Web Application Firewall) می‌تواند الگوهای شناخته‌شدهٔ حملهٔ SQL Injection را تشخیص دهد و درخواست‌های مخرب را قبل از رسیدن به برنامه بلاک کند. لایه‌ای که در فایروال نرم‌افزاری در سرور و فایروال ابری در مقابل سنتی توضیح داده‌ام.

لایه هفتم: پایش و لاگ‌گیری

هر حملهٔ SQL Injection، ردی در لاگ‌ها می‌گذارد. با پایش منظم لاگ‌های دسترسی و لاگ‌های دیتابیس، می‌توانید الگوهای مشکوک را زودتر شناسایی کنید. تمرکز روی پایش در چگونه لاگ حملات سایت را بررسی کنیم؟.

Prepared Statements: قلب دفاع مدرن

در بین هفت لایه، Prepared Statements را باید عمیق‌تر باز کنیم. مکانیزم این تکنیک، در واقع جدا کردن ساختار کوئری از داده است. وقتی یک Prepared Statement اجرا می‌شود، دیتابیس دو مرحله را طی می‌کند:

مرحله اول: کامپایل ساختار کوئری

دیتابیس ابتدا کوئری را با جای‌خالی (placeholder) دریافت می‌کند و ساختار آن را کامپایل می‌کند. در این مرحله، هیچ داده‌ای از کاربر دخیل نیست. ساختار، ثابت است.

مرحله دوم: اتصال داده به جای‌خالی

بعد از کامپایل، دادهٔ کاربر به جای‌خالی‌ها متصل می‌شود. مهم‌ترین نکته: دیتابیس، دادهٔ کاربر را همیشه به‌عنوان داده می‌بیند، نه به‌عنوان کد. حتی اگر داده شامل کاراکترهای SQL باشد، این کاراکترها بی‌اثر می‌شوند.

این جدایی، ریشهٔ امنیت Prepared Statements است. برخلاف escape کردن (که می‌کوشد کاراکترهای خطرناک را حذف کند)، Prepared Statements نیازی به حدس زدن کاراکترهای خطرناک ندارد؛ فقط داده را از کد جدا می‌کند. این تفاوت فلسفی، دلیل اصلی برتری این تکنیک در برابر تمام روش‌های سنتی است.

SQL Injection در وردپرس: چرا وردپرس امن است؟

وردپرس در سطح هستهٔ خود، بسیار مقاوم در برابر SQL Injection است. سه دلیل اصلی:

دلیل اول: استفاده از wpdb::prepare

هستهٔ وردپرس، توابع و APIهایی مثل $wpdb->prepare() را برای Prepared Statements فراهم می‌کند و خودش هم در همه‌جا از آن استفاده می‌کند. اگر توسعه‌دهنده از همین API استفاده کند، برنامه به‌طور پیش‌فرض در برابر SQL Injection مقاوم است.

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

APIهای وردپرس مثل $wpdb->insert() و $wpdb->update() به‌طور خودکار ورودی‌ها را پاک‌سازی می‌کنند. اگر توسعه‌دهنده از این توابع به‌جای SQL دستی استفاده کند، ریسک تقریباً صفر است. راهنمای کار با این APIها در کار با Options API در کدنویسی وردپرس و کدنویسی کوئری‌های سفارشی آمده است.

دلیل سوم: بازبینی مخزن افزونه‌ها

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

پس چرا همچنان سایت‌های وردپرسی هک می‌شوند؟ چون منبع آسیب‌پذیری معمولاً خودِ وردپرس نیست، بلکه یکی از این سه است: افزونهٔ سفارشی که از Prepared Statements استفاده نمی‌کند، قالب سفارشی که کوئری دستی می‌زند، یا افزونهٔ نال که کد آلوده دارد. نکات کامل در چگونه دیتابیس وردپرس را امن کنیم؟ و بهترین روش‌های امنیت MySQL.

اشتباهات رایج در دفاع و اشتباهات مهاجم

در بازبینی کدهای مختلف، چند الگوی اشتباه را زیاد دیده‌ام:

  • اعتماد به escape کردن به‌تنهایی: توابع escape مثل mysqli_real_escape_string در صورت استفادهٔ درست مؤثرند، اما اگر برنامه‌نویس پارامتر دیگری مثل charset را اشتباه تنظیم کند، ممکن است escape دور زده شود. Prepared Statements این ریسک را ندارد.
  • پنهان‌کردن خطا بدون اصلاح کد: بعضی برنامه‌نویس‌ها فقط پیام‌های خطای دیتابیس را غیرفعال می‌کنند و فکر می‌کنند امن شده‌اند. اما Blind SQL Injection همچنان کار می‌کند.
  • استفاده از کاربر دیتابیس با دسترسی کامل: حتی اگر برنامه ناامن باشد، محدود کردن دسترسی کاربر دیتابیس می‌تواند آسیب را محدود کند. اما خیلی‌ها از همان کاربر روت MySQL استفاده می‌کنند.
  • نادیده گرفتن ترافیک خروجی: بعضی حملات موفق، خروجی ندارند — فقط داده را به سرور مهاجم می‌فرستند. بدون پایش ترافیک خروجی، این حملات ماه‌ها بی‌صدا می‌مانند.
  • فرض این‌که چون کاربر لاگین‌نکرده، در امان است: بعضی endpointها مثل فرم جستجو، بدون لاگین هم کار می‌کنند و همین‌ها هدف آسان‌تری برای مهاجم هستند.

پاسخ به پرسش‌های پرتکرار درباره تزریق SQL

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

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

تفاوت SQL Injection و XSS چیست؟

هر دو حمله‌های تزریقی هستند، ولی هدفشان متفاوت است. SQL Injection در لایهٔ دیتابیس اجرا می‌شود و هدفش نفوذ به داده‌های ذخیره‌شده است. XSS (Cross-Site Scripting) در لایهٔ مرورگر اجرا می‌شود و هدفش اجرای کد جاوااسکریپت در مرورگر قربانی است. دفاع در برابر SQL Injection با Prepared Statements و در برابر XSS با Escaping خروجی انجام می‌شود. توضیح کامل XSS در حملات XSS چیست و چگونه دفع می‌شود؟

آیا وردپرس در برابر SQL Injection آسیب‌پذیر است؟

هستهٔ وردپرس بسیار مقاوم است و از Prepared Statements در همه‌جا استفاده می‌کند. اما آسیب‌پذیری‌ها معمولاً از افزونه‌ها و قالب‌های سفارشی می‌آیند، به‌خصوص وقتی توسعه‌دهنده از $wpdb به‌درستی استفاده نمی‌کند. اگر فقط از هستهٔ وردپرس و افزونه‌های معتبر مخزن رسمی استفاده کنید، احتمال حملهٔ موفق نزدیک به صفر است.

چگونه بفهمم سایتم مورد حمله SQL Injection قرار گرفته؟

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

آیا Prepared Statements کامل امن است؟

Prepared Statements مؤثرترین دفاع شناخته‌شده در برابر SQL Injection است، ولی صد‌درصد نیست. اگر برنامه‌نویس از جای‌خالی‌ها به‌درستی استفاده نکند (مثلاً نام جدول یا ستون را از ورودی کاربر بگذارد)، می‌تواند همچنان آسیب‌پذیر باشد. نام جدول و ستون باید همیشه از فهرست ثابت انتخاب شود، نه از ورودی کاربر. همچنین اگر از ORM استفاده می‌کنید، باید مطمئن شوید که پرس‌وجوهای خام را با ورودی کاربر مستقیم نمی‌سازید.

آیا محدود کردن دسترسی کاربر دیتابیس کافی است؟

خیر. محدود کردن دسترسی کاربر دیتابیس، لایهٔ مهمی است ولی جای Prepared Statements را نمی‌گیرد. حتی با دسترسی محدود، مهاجم می‌تواند داده‌های مجاز (مثل اطلاعات کاربران) را بدزدد یا تغییر دهد. دفاع باید چندلایه باشد: Prepared Statements + Validation + محدودیت دسترسی + WAF + پایش.

چرا بعد از همه این سال‌ها، SQL Injection هنوز در لیست OWASP Top 10 است؟

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

سخن پایانی: چرا SQL Injection هنوز زنده است

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

پیشنهاد عملی من سه گام است. اول، اگر کد PHP می‌نویسید، از همین امروز Prepared Statements را استاندارد کنید، نه استثنا. دوم، در پروژه‌های وردپرس، همیشه از توابع وردپرس مثل $wpdb->prepare() استفاده کنید، نه SQL دستی. سوم، سه‌ماهه یک‌بار کد پروژه‌های مهم خودتان را بازبینی کنید و بدنبال کوئری‌های دستی بگردید؛ در تجربهٔ من، همیشه یکی دو مورد پیدا می‌شود که فراموش شده‌اند.

اگر تجربه‌ای از مواجهه با SQL Injection در پروژه‌های خودتان دارید — به‌خصوص اگر با نوع Blind یا Out-of-band روبه‌رو شده‌اید — برایم بنویسید. همین تجربه‌های میدانی، فهم ما از این حمله و دفاع در برابر آن را کامل‌تر می‌کند. 🔐