چند سال پیش، روی یک سایت فروشگاهی که تازه تحویلش را گرفته بودم، شبی در لاگ MySQL دنبال یک مشکل ظاهراً ساده گشتم. جلوی چشمانم، هزاران کوئری عجیب ثبت شده بود: کوئری‌هایی که در آن‌ها کلماتی مثل UNION SELECT و INFORMATION_SCHEMA تکرار می‌شدند و همه از یک IP می‌آمدند. یک افزونه کوچک که سال‌ها بدون به‌روزرسانی روی سایت مانده بود، ورودی GET را بدون هیچ فیلتری به کوئری می‌چسباند. آن شب فهمیدم تزریق SQL (SQL Injection) چیز عجیبی نیست؛ همان ضرب‌المثل قدیمی است که با یک بی‌دقتی چند خطی، درِ خانه را باز می‌کند. این مقاله از منظر کسی نوشته شده که بیش از یک بار روی سایت‌های در معرض خطر، ریشه‌یابی این حمله را انجام داده و یاد گرفته که دفاع واقعی، پروتکل است نه وصله.

تزریق SQL دقیقاً چیست؟

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

  1. مرز بین داده و کد شکسته می‌شود. وقتی برنامه در حال ساخت یک کوئری است، اگر داده ورودی کاربر بدون جدا‌سازی صحیح به متن کوئری چسبانده شود، دیتابیس نمی‌تواند تشخیص دهد کدام بخش کد برنامه است و کدام بخش داده کاربر. نتیجه: کاربر در نقش نویسنده کوئری می‌نشیند.
  2. حمله در لایه اپلیکیشن رخ می‌دهد، نه در لایه دیتابیس. دیتابیس همان کاری را می‌کند که به او گفته شده. مقصر، برنامه‌ای است که کوئری را بدون پارامترسازی ساخته است.
  3. پیامدها فقط خواندن داده نیست. در پیکربندی‌های ضعیف، مهاجم می‌تواند داده را تغییر دهد، حذف کند، فایل روی سرور بخواند، در برخی موارد فرمان سیستم‌عامل اجرا کند و حتی از دیتابیس به‌عنوان پل برای نفوذ بیشتر استفاده کند.

اصطلاح تزریق SQL (SQL Injection) در سال ۱۹۹۸ توسط فیزیکدان و مهاجم امنیتی، Jeff Forristal، در مقالهای در Phrack معرفی شد. از آن زمان، آموزش‌ها، چارچوب‌های امن و استانداردهای کدنویسی رشد کرده‌اند، اما این حمله هم‌چنان در رتبه‌های بالای فهرست OWASP (Open Web Application Security Project) می‌ماند. علتش در بخش بعدی روشن می‌شود.

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

چرا این حمله هنوز زنده است؟

پرسش درستی که در جلسات امنیتی زیاد می‌شنوم: اگر راه‌حل تزریق SQL دهه‌هاست که شناخته شده، چرا هنوز شاهد نفوذهای بزرگ هستیم؟ پاسخ، سه‌لایه دارد:

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

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

آناتومی یک حمله: از ورودی تا داده

بیایید یک مسیر واقعی را با هم طی کنیم. فرض کنید یک صفحه ورود ساده داریم که با PHP و MySQL نوشته شده و به‌صورت ناامن با کوئری زیر کار می‌کند:

$user = $_POST['username'];
$pass = $_POST['password'];
$sql  = "SELECT * FROM users WHERE username = '$user' AND password = '$pass'";
$result = $mysqli->query($sql);

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

' OR '1'='1' -- 

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

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

بخش OR '1'='1' همیشه درست است؛ و بخش بعدی با -- به کامنت تبدیل می‌شود. نتیجه: دیتابیس، اولین کاربر را برمی‌گرداند و مهاجم بدون داشتن رمز، وارد سیستم می‌شود. این ساده‌ترین شکل حمله است، اما همین کافی است تا در یکی از پروژه‌های قدیمی که بازبینی کردم، امکان ورود به پنل مدیریت را فراهم کند.

نسخه پیشرفته‌تر این حمله، جای درست را می‌زند: به‌جای دور زدن احراز هویت، داده استخراج می‌کند. با UNION-based به مهاجم اجازه داده می‌شود کوئری دلخواهش را به نتیجه SELECT فعلی بچسباند، و با Time-based می‌تواند وجود یا عدم وجود یک داده را از تأخیر پاسخ سرور استنتاج کند.

خانواده تزریق SQL: شش چهره یک آسیب‌پذیری

در ادبیات امنیتی، SQL Injection یک خانواده است، نه یک تکنیک. شش عضو اصلی این خانواده را با هم مرور می‌کنیم:

نوعچگونه کار می‌کندسختی کشفابزار دفاع
Classic (In-band)نتیجه در پاسخ HTTP بازمی‌گردد؛ ساده‌ترین حالتآسانPrepared Statements
Union-basedنتیجه کوئری مهاجم با UNION به نتیجه اصلی چسبانده می‌شودمتوسطPrepared Statements + محدودسازی نمایش خطا
Error-basedمهاجم از پیام‌های خطا برای استخراج داده استفاده می‌کندمتوسطخاموش‌سازی خطای خام + فیلتر لاگ
Boolean-based (Blind)مهاجم با سؤال بله/خیر، بیت‌به‌بیت داده را استنتاج می‌کندسختPrepared Statements + WAF
Time-based (Blind)با SLEEP یا BENCHMARK تأخیر ساخته می‌شودسختPrepared Statements + محدودسازی زمان اجرا
Second-orderورودی مخرب ابتدا ذخیره و در کوئری بعدی اجرا می‌شودبسیار سختاعتبارسنجی در زمان ذخیره و بازیابی

دو نوع آخر، یعنی Time-based و Second-order، همان‌هایی هستند که در پروژه‌های واقعی بیشترین درد را ساختند. در یک سیستم انبار، مهاجم نامه یک محصول را طوری نوشته بود که فقط وقتی گزارش‌گیری خودکار شبانه اجرا می‌شد، تزریق فعال می‌شد. تا سه ماه کسی متوجه نشد، چون در بازه زمانی معمول، حمله بی‌صدا بود.

شکار و تشخیص: چطور بفهمیم قربانی شده‌ایم؟

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

در پروژه‌های واقعی، من به نشانه‌های غیرفعال زیر حساس شده‌ام:

  • کوئری‌های عجیب در general_log یا slow_query_log. عبارت‌هایی مثل UNION، INFORMATION_SCHEMA، SLEEP، BENCHMARK یا کاراکترهای تک‌کوتیشن در ورودی یک درخواست.
  • افزایش غیرطبیعی زمان پاسخ سرور در بازه‌های منظم. نشانه احتمالی Time-based Blind است.
  • پیام‌های خطای MySQL که مستقیم به کاربر نشان داده می‌شوند. این پیام‌ها برای مهاجم، طلاست.
  • افزایش ترافیک از IPهای ناشناس روی URLهایی که فرم دارند. معمولاً اسکنرهای خودکار وارد مرحله تست شده‌اند.
  • ظهور جداول یا ردیف‌های عجیب در دیتابیس. نشانه Second-order یا حمله‌ای که به مرحله نهایی رسیده است.

برای پایش منظم، راهنمای بهترین افزونه‌های امنیتی وردپرس نقطه شروع خوبی است؛ مخصوصاً بخشی که به اسکنر بدافزار و لاگ فعالیت می‌پردازد. علاوه بر آن، فعال‌کردن general_log در MySQL به‌صورت دوره‌ای و بازبینی آن — نه به‌صورت دائمی که سرور را کند می‌کند — یک عادت حرفه‌ای است.

دفاع لایه‌ای: پروتکل، نه وصله

دفاع در برابر تزریق SQL، یک وصله نیست؛ یک پروتکل چندلایه است. در چارچوبی که در پروژه‌ها استفاده می‌کنم، پنج لایه وجود دارد:

  1. لایه پارامترسازی: Prepared Statements که مرز داده و کد را در سطح پروتکل دیتابیس تثبیت می‌کند.
  2. لایه انتزاع: ORM که کوئری‌ها را به API سطح‌بالا تبدیل می‌کند و امکان تزریق را کاهش می‌دهد (اما حذف نمی‌کند).
  3. لایه دسترسی: اصل کمترین دسترسی برای کاربر MySQL.
  4. لایه پیکربندی: سرسختی‌سازی MySQL و شبکه.
  5. لایه پایش: WAF، لاگ و هشدار.

هر لایه به‌تنهایی کافی نیست. مثال بارزی که در یکی از پروژه‌ها دیده‌ام: تیم کاملاً از Prepared Statements استفاده می‌کرد، اما کاربر MySQL همان کاربر root بود. یک آسیب‌پذیری در یک بخش دیگر، امکان اجرای کوئری دلخواه را برای مهاجم فراهم کرد و چون آن کاربر همه دسترسی‌ها را داشت، پیامد، حذف چند جدول کلیدی بود. لایه پارامترسازی، مسئله تزریق را حل کرده بود؛ اما لایه دسترسی، فاجعه ناشی از یک آسیب‌پذیری دیگر را تشدید کرد.

Prepared Statements: قلب دفاع

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

در PHP با MySQLi، نسخه امن کوئری قبلی چنین می‌شود:

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

سه نکته فنی مهم در این قطعه کد پنهان است:

  • علامت سؤال، جای داده را نگه می‌دارد. هیچ کاراکتری از ورودی کاربر، به متن کوئری اضافه نمی‌شود.
  • نوع داده تعیین می‌شود. با ss به دیتابیس گفته می‌شود که هر دو پارامتر از نوع رشته هستند؛ دیتابیس، هر محتوایی را به‌عنوان رشته در نظر می‌گیرد و تزریق ساختاری را ناممکن می‌کند.
  • حتی اگر مهاجم کاراکترهای ویژه تزریق کند، دیتابیس آن‌ها را به‌عنوان داده می‌بیند. همین، تفاوت بنیادی بین escape کردن و parameterizing است.

در PDO، الگو مشابه است:

$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :user AND password_hash = :hash");
$stmt->execute([':user' => $user, ':hash' => $hash]);

در Python با psycopg یا mysql-connector، الگو با %s یا ? است:

cursor.execute("SELECT * FROM users WHERE username = %s AND password_hash = %s", (user, password_hash))

در Java با JDBC:

PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ?");
stmt.setString(1, user);
ResultSet rs = stmt.executeQuery();

در همه این زبان‌ها، الگو یکی است: پرس‌وجو با جای‌نگهدار ساخته می‌شود، داده‌ها جدا bind می‌شوند. این تنها روشی است که در سطح پروتکل، دیتابیس را از تمایز داده و کد مطمئن می‌کند.

Prepared Statement یک ویژگی زبانی نیست؛ یک قرارداد پروتکلی است که دیتابیس و درایور آن را تضمین می‌کنند.

ORM: نجات‌دهنده یا توهم امنیت؟

ORM (Object-Relational Mapping) به‌عنوان یکی از لایه‌های دفاعی عمل می‌کند، اما با یک شرط بزرگ: تا زمانی که از API سطح‌بالای آن استفاده کنید. به‌محض این‌که به هر شکلی به کوئری خام پناه ببرید، لایه دفاعی ORM از بین می‌رود.

در پروژه‌های Django، Django ORM به‌طور پیش‌فرض کوئری‌ها را پارامترسازی می‌کند. اما یک نکته ظریف وجود دارد: برخی توسعه‌دهندگان برای انجام کوئری‌های پیچیده، از extra() یا RawSQL استفاده می‌کنند. اگر در آن‌جا متغیر کاربر را به‌صورت رشته‌ای وارد کنند، لایه امنیتی از بین می‌رود. همین موضوع در Laravel Eloquent با DB::raw() و در Hibernate با NativeQuery صادق است.

مثال Django برای نمایش تفاوت:

# امن
User.objects.raw("SELECT * FROM app_user WHERE username = %s", [username])

# ناامن
User.objects.raw(f"SELECT * FROM app_user WHERE username = '{username}'")

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

اصل کمترین دسترسی: لایه‌ای که فراموش می‌شود

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

پروتکل عملی:

  • کاربر اپلیکیشن را از root جدا کنید. حتی یک کاربر دیگر با نام متفاوت، اولین قدم جدی است.
  • فقط دسترسی‌های لازم را بدهید. اگر برنامه فقط SELECT و INSERT می‌کند، GRANT UPDATE یا DROP را ندهید.
  • دسترسی را به دیتابیس محدود کنید. با GRANT ... ON specific_db.* فقط به دیتابیس موردنیاز دسترسی بدهید، نه همه دیتابیس‌ها.
  • دسترسی از راه دور را ببندید. اگر برنامه روی همان سرور است، اجازه اتصال فقط از 127.0.0.1 کافی است.
  • برای عملیات حساس، کاربر جداگانه بسازید. مثلاً یک کاربر فقط برای گزارش‌گیری با SELECT محدود به چند جدول.

این لایه، از آن دسته لایه‌هایی است که در روزهای عادی به چشم نمی‌آید، اما در روز حادثه، تفاوت بین از دست دادن چند ردیف و از دست دادن کل دیتابیس را می‌سازد.

سرسختی‌سازی MySQL و بستر دیتابیس

علاوه بر پارامترسازی و کنترل دسترسی، چند اقدام سرسخت‌سازی (Hardening) در سطح MySQL و بستر آن، عمق دفاع را افزایش می‌دهد:

  • غیرفعال‌سازی LOCAL INFILE. با local_infile = 0 در my.cnf، یکی از مسیرهای خواندن فایل از سرور بسته می‌شود.
  • غیرفعال‌سازی FILE privilege. کاربر اپلیکیشن نباید این دسترسی را داشته باشد.
  • محدودسازی اجرای همزمان کوئری‌های سنگین. تنظیم max_execution_time در MySQL، اثر حمله Time-based را کاهش می‌دهد.
  • رمزنگاری اتصال (TLS). جلوگیری از شنود کوئری‌ها در مسیر شبکه.
  • پشتیبان‌گیری منظم و تست‌شده. اگر بدترین اتفاق افتاد، بازیابی سریع، تفاوت بین یک روز خرابی و یک فاجعه است.
  • پایش لاگ خطا و general_log به‌صورت دوره‌ای. ابزارها در راهنمای انتخاب هاست معرفی شده‌اند؛ بخشی از تصمیم هاست، دسترسی به همین قابلیت‌هاست.

در سطح وب‌سرور، قواعدی مثل mod_security در Apache یا یک WAF (Web Application Firewall) در سطح CDN، لایه ششم دفاعی هستند. اما این لایه‌ها بخشی از پروتکل اصلی نیستند؛ نقششان کاهش بار روی لایه‌های پایین است. اگر برنامه در لایه پارامترسازی ضعیف باشد، WAF نمی‌تواند به‌تنهایی نجات دهد.

الگوهای غلطی که باید کنار بگذاریم

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

  • escape کردن به‌عنوان دفاع اصلی. mysqli_real_escape_string و معادل‌های آن، راه‌حل کاملی نیستند. escape کردن در شرایطی که کدگذاری دیتابیس اشتباه تنظیم شده باشد، شکست می‌خورد. پارامترسازی، جایگزین واقعی است.
  • اعتماد به blacklist کاراکترها. فیلترکردن کاراکترهایی مثل کوتیشن، آپاستروف یا کلماتی مثل UNION، همیشه یک راه دور زدن دارد: با encoding متفاوت، با کاراکترهای یونیکد خاص یا با تکنیک‌های مبتنی بر زمان. blacklist، مسئله را کوچک می‌کند اما حذف نمی‌کند.
  • نمایش خطای خام به کاربر. پیام‌هایی مثل You have an error in your SQL syntax near ...، برای مهاجم نقش نقشه گنج را بازی می‌کنند. در محیط تولید، خطاها باید به لاگ بروند، نه به کاربر.
  • ساخت کوئری با sprintf یا الحاق رشته. حتی اگر برنامه‌نویس مطمئن باشد ورودی امن است، اصل باید این باشد: هیچ کوئری رشته‌ای بدون پارامتر ساخته نمی‌شود.
  • استفاده از حساب root در اپلیکیشن. این کار در محیط توسعه راحت است، اما در تولید، ریسک را چند برابر می‌کند.
  • نادیده‌گرفتن ورودی‌های غیرمستقیم. کوکی‌ها، هدرها، پارامترهای اضافی URL، حتی فیلدهای پنهان در فرم، همه ورودی هستند و باید پارامترسازی شوند.
امنیت، از جنس نبود آسیب‌پذیری نیست؛ از جنس نبود اثر مخرب است. برنامه‌ای که آسیب‌پذیری دارد اما دیتابیسش خوانده نمی‌شود، امن‌تر از برنامه‌ای است که آسیب‌پذیری دارد و دیتابیسش همه‌چیز را به مهاجم می‌دهد.

تزریق SQL در وردپرس: بسترهای خاص

در دنیای وردپرس، تزریق SQL معمولاً از دو منبع می‌آید: افزونه‌های ضعیف و قالب‌های نوشته‌شده با الگوهای قدیمی. خود هسته وردپرس، از API $wpdb و متدهای prepare استفاده می‌کند و اگر توسعه‌دهنده استانداردها را رعایت کند، سطح حمله بسته است. اما همین‌جا سه تله رایج وجود دارد:

  1. استفاده نادرست از $wpdb->query(). اگر متغیر کاربر بدون $wpdb->prepare() به کوئری چسبانده شود، دقیقاً همان آسیب‌پذیری کلاسیک رخ می‌دهد. الگوی امن چنین است:
$sql = $wpdb->prepare("SELECT * FROM {$wpdb->posts} WHERE post_author = %d AND post_status = %s", $author_id, 'publish');
$results = $wpdb->get_results($sql);
  1. افزونه‌های رهاشده. یکی از شایع‌ترین مسیرهای نفوذ در سایت‌های وردپرسی، افزونه‌هایی هستند که سال‌ها به‌روزرسانی نشده‌اند و یک آسیب‌پذیری شناخته‌شده در آن‌ها باقی مانده است. راهنمای دانلود افزونه مطمئن وردپرس دقیقاً برای همین سناریو نوشته شده؛ پیش از نصب هر افزونه‌ای، بررسی سابقه امنیتی و به‌روزرسانی‌ها اولین قدم است.
  2. قالب‌های دست‌کاری‌شده. قالب‌هایی که از منابع نامعتبر دانلود می‌شوند، گاهی کوئری‌های ناامن یا بک‌دور در فایل‌های خود دارند. روش تشخیص را در شناسایی قالب استاندارد وردپرس توضیح داده‌ام.

علاوه بر این، فعال‌کردن WAF در سطح CDN یا در سطح سرور، لایه‌ای است که برای سایت‌های وردپرسی بسیار توصیه می‌شود. ترکیب این لایه با به‌روزرسانی منظم افزونه‌ها، بخش بزرگی از ریسک را از بین می‌برد. اگر روی وردپرس سایت جدی دارید، وردپرس چیست و چگونه شروع کنیم نقطه شروع خوبی برای درک بستر است.

انضباط مهندسی در تیم

دفاع فنی فقط نصف راه است. نصف دیگر، انضباط تیمی است. در تیم‌هایی که با آن‌ها کار کرده‌ام، این سه قاعده بیشترین اثر را داشته‌اند:

  • کوئری‌های خام در بازبینی کد، پرچم قرمز هستند. هر کوئری که از ORM یا لایه انتزاعی بیرون بزند، نیاز به توضیح کتبی دارد و در بازبینی، بازبینی جدا می‌شود.
  • اجرای ابزارهای SAST (Static Application Security Testing) در CI/CD. ابزارهایی مثل Semgrep یا CodeQL می‌توانند الگوهای ناامن کوئری را در کد شناسایی کنند. این ابزارها جای مهندس را نمی‌گیرند، اما چشم اضافه‌ای هستند.
  • ممیزی منظم دیتابیس و دسترسی‌ها. هر سه ماه، فهرست کاربران MySQL، دسترسی‌هایشان و اتصالات فعال بازبینی شود. کاربران بلااستفاده، پاک شوند.

یک نکته اضافه: در تیم‌هایی که از چند زبان استفاده می‌کنند، بهتر است قاعده‌ای مشترک نوشته شود: هر لایه داده، مسئول پارامترسازی خودش است و نمی‌تواند به لایه بالایی تکیه کند. این تفکیک مسئولیت، در کد بازبینی، هم شفاف‌تر است و هم قابل تست.

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

پرسش‌هایی که در جلسات مشاوره زیاد می‌شنوم، با پاسخ کوتاه و عملی:

آیا استفاده از Prepared Statements کافی است؟

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

آیا WAF جایگزین اصلاح کد است؟

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

آیا NoSQL دیتابیس‌ها هم در برابر تزریق آسیب‌پذیرند؟

بله. NoSQL Injection یک کلاس جداگانه است که در MongoDB، CouchDB و دیگر پایگاه‌های داده NoSQL دیده می‌شود. اصول دفاعی مشابه است: پارامترسازی و اعتبارسنجی. اما جزئیات فنی، متفاوت است.

چگونه می‌توانم اطمینان پیدا کنم که کد فعلی سایت امن است؟

سه مسیر موازی: ابزارهای SAST روی پایگاه کد، تست نفوذ روی محیط آزمایشی با ابزاری مثل SQLMap، و بازبینی دستی کوئری‌های خام. هیچ‌کدام به‌تنهایی کافی نیست؛ ترکیب سه‌گانه اعتمادپذیر است.

اگر سایت وردپرسی روی هاست اشتراکی باشد، چه کار کنم؟

سه اولویت: به‌روزرسانی افزونه‌ها و قالب‌ها، حذف افزونه‌های بلااستفاده، و استفاده از WAF سطح CDN مثل Cloudflare. اکثر هاست‌های اشتراکی کنترل لازم برای تنظیم دقیق MySQL را به شما نمی‌دهند؛ بنابراین باید در لایه برنامه و لایه CDN دفاع کنید.

تفاوت escape کردن و parameterizing چیست؟

escape کردن، داده را برای چسباندن به کوئری آماده می‌کند. parameterizing، داده را از کوئری جدا نگه می‌دارد و مرز داده/کد را در پروتکل تعیین می‌کند. اولی همیشه با شرایط محیطی در خطر است، دومی مرز ساختاری می‌سازد.

درسی که سال‌ها بعد هم تکرار می‌شود

تزریق SQL یکی از آن آسیب‌پذیری‌هایی است که وقتی رخ می‌دهد، همه می‌پرسند چطور ممکن است. پاسخ، همیشه یکسان است: یک جا، یک نفر، یک خط کد، اصل پارامترسازی را رعایت نکرده. دفاع واقعی، نه در لحظه حمله، بلکه در لحظه نوشتن کد اتفاق می‌افتد. اگر امروز سه کار را انجام دهید — پارامترسازی همه کوئری‌ها، جداکردن کاربر MySQL اپلیکیشن از root، و پایش لاگ به‌صورت دوره‌ای — در برابر بخش بزرگی از این خانواده حمله، بسته‌اید.

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