خانه/ امنیت/ تزریق SQL

تزریق SQL: وقتی ورودی کاربر به‌جای داده، به کد تبدیل می‌شود

پیشرفته ۱۵ دقیقه مطالعه

تصور کن یک فرم ورود ساده داری و کد برنامه (مثلاً در C#‎ یا پایتون)، پرس‌وجوی بررسی کاربر را با الحاق مستقیم رشته‌ها می‌سازد:

csharp
// کد برنامه — این الگو بسیار خطرناک است:
string sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "';";

در حالت عادی، اگر کاربر ali و رمز 12345 را وارد کند، این پرس‌وجوی کاملاً بی‌خطر ساخته می‌شود:

امتحانش کن — ورود عادی و درست
فقط ردیف کاربر ali برگشت — دقیقاً همان چیزی که انتظار داریم.

حالا یک کاربر مخرب چه می‌کند؟

فرض کن به‌جای نام کاربری، این رشته را در فیلد username وارد کند: ' OR '1'='1' --. چون کد برنامه این مقدار را بدون هیچ پاک‌سازی‌ای مستقیم داخل متن پرس‌وجو می‌چسباند، پرس‌وجوی نهایی این‌طور می‌شود:

امتحانش کن — همان کد، با ورودی مخرب
هر دو ردیف برگشت — از جمله حساب admin با is_admin = 1 — بدون این‌که هیچ رمز درستی وارد شده باشد! اتفاقی که افتاد: '1'='1' همیشه درست است، پس شرط WHERE برای همه‌ی ردیف‌ها برقرار می‌شود؛ و -- بقیه‌ی خط (از جمله بررسی رمز عبور) را به یک توضیح (Comment) تبدیل کرد و کاملاً نادیده گرفته شد. کاربر مخرب بدون دانستن هیچ رمزی، وارد شد — و اگر این کد پشت یک فرم لاگین واقعی بود، دقیقاً به همین راحتی حساب مدیر را تصاحب می‌کرد.

ریشه‌ی مشکل: مرز بین «کد» و «داده» گم شده

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

راه‌حل واقعی: پرس‌وجوی پارامتری (Parameterized Query)

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

csharp
// نسخه‌ی امن با ADO.NET:
var cmd = new SqlCommand("SELECT * FROM users WHERE username = @u AND password = @p", connection);
cmd.Parameters.AddWithValue("@u", username);
cmd.Parameters.AddWithValue("@p", password);

معادل همین ایده در خودِ T-SQL با sp_executesql:

sql
EXEC sp_executesql
  N'SELECT * FROM users WHERE username = @u AND password = @p',
  N'@u NVARCHAR(50), @p NVARCHAR(50)',
  @u = @username, @p = @password;

حتی اگر کاربر مخرب همان ' OR '1'='1' -- را به‌عنوان مقدار @u بفرستد، موتور آن را دقیقاً به‌عنوان یک رشته‌ی تحت‌اللفظی (یعنی به‌دنبال کاربری با آن نام عجیب می‌گردد) در نظر می‌گیرد — نه به‌عنوان بخشی از ساختار SQL. نتیجه: هیچ رکوردی مطابقت پیدا نمی‌کند و ورود رد می‌شود.

لایه‌های دفاعی دیگر

  • Stored Procedure با پارامتر: اگر پروسیجرها را طوری بنویسی که از پارامترهای واقعی استفاده کنند (نه ساختن دستور پویا با الحاق رشته داخل خودِ پروسیجر)، از همان ابتدا در برابر این حمله ایمن هستند.
  • اصل حداقل دسترسی: همان‌طور که در درس قبل گفتیم، اگر Userِ برنامه فقط db_datareader باشد، حتی یک تزریق موفق هم نمی‌تواند داده‌ای حذف یا تغییر دهد.
  • اعتبارسنجی ورودی: محدودکردن طول و نوع کاراکترهای مجاز (مثلاً نام کاربری فقط حروف و عدد)، لایه‌ی دفاعی اضافه‌ای است — اما هرگز نباید تنها راه دفاع باشد.
  • ORM‌ها (مثل Entity Framework): اکثر ORMها به‌طور پیش‌فرض از پرس‌وجوی پارامتری استفاده می‌کنند — اما اگر داخل آن‌ها هم SQL خام با الحاق رشته بنویسی، همان خطر برمی‌گردد.

جمع‌بندی این درس و فصل امنیت

  • تزریق SQL زمانی رخ می‌دهد که ورودی کاربر بدون پارامترسازی، مستقیم داخل متن دستور SQL چسبانده شود.
  • با یک ورودی ساده مثل ' OR '1'='1' --، مهاجم می‌تواند کل بررسی رمز عبور را کاملاً دور بزند — چیزی که خودمان زنده دیدیمش.
  • راه‌حل قطعی، پرس‌وجوی پارامتری (Parameterized Query) است — نه فیلترکردن دستی کاراکترهای خطرناک.
  • Users، Roles و Permissions که در این فصل یاد گرفتیم، لایه‌ی دفاعی دوم را می‌سازند: حتی اگر تزریق موفق شود، اصل حداقل دسترسی خسارت را محدود می‌کند.

فصل بعدی سراغ چند پروژه‌ی واقعی می‌رود تا هرچه در این ۱۱ فصل یاد گرفتی را کنار هم، روی مسئله‌های واقعی به کار بگیری.