ساخت کوئری های پویا (قسمت پایانی)

20 اسفند 1397
درسنامه درس 5 از سری مقابله با SQL Injection
SQL-Injection-dynamic-queries

با سلام و احترام خدمت شما همراهان گرامی روکسو، در این قسمت از سری آموزشی مقابله با SQL Injection می خواهیم در رابطه با کوئری هایی صحبت کنیم که به صورت پویا (dynamic) ساخته می شوند.

کوئری های پویا (Dynamic queries)

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

در چنین حالت هایی امکان استفاده از placeholder ها وجود ندارد بنابراین به مکانیسم دیگری نیاز داریم. اگر قسمت مورد نظر سایت شما (مثلا جست و جوی پیشرفته) پیچیدگی های زیادی نداشته باشد می توانید از query builder (به معنی سازنده ی کوئری) ها استفاده کنید. مکانیسم عمل این query builder ها به شکل زیر است:

$query = $users = DB::table('users')->select('*');
if ($fname = input::get('first_name'))
{
    $query->where('first_name = ?', $fname);
}
if ($lname = input::get('last_name'))
{
    $query->where('last_name = ?', $lname);
}
// و همینطور الی آخر
$results = $query->get();

اما اکثر اوقات کوئری هایی داریم که پیچیدگی بسیار بیشتری دارند و استفاده از query builder ها آنچنان سخت می شود که دیگر توجیه عقلانی ندارد. ما همیشه میدانیم که تمام قسمت های پویای یک کوئری با placeholder ها وارد کوئری اصلی می شوند اما برای این حالت یک حقه ی بسیار خوب سراغ داریم:

در وب سایت stackoverflow.com سوالی در این رابطه پرسیده شده است و جواب آن (با آنکه ساده است) از جواب های زیرکانه و بسیار خوبی می باشد که در این چند سال دیده ام. فرد سوال کننده گفته است قصد طراحی یک سیستم جست و جو در وب سایتش را دارد که فاکتور های مختلف آن را کاربران تعیین می کنند. از طرفی ممکن است کاربر برخی از این فاکتور ها را رها کند (به طور مثال سن و سال برایش مهم نباشد). پرسشگر می خواهد بداند چطور ممکن است در این شرایط کوئری ای بسازد که تمام حالات مختلف (مانند رها کردن سن و سال توسط کاربر) را داشته باشد و قابل اجرا نیز باشد. مثالی که خود پرسشگر ذکر کرده است از این قرار است:

$sql = 'SELECT * FROM people WHERE first_name = :first_name AND last_name = :last_name AND age = :age AND sex = :sex';
$query = $db->prepare($sql);
$query->execute(array(':first_name' => 'John', ':age' => '27');

جوابی که دریافت کرده است کد زیر می باشد:

SELECT * FROM people 
WHERE (first_name = :first_name or :first_name is null)
AND (last_name = :last_name or :last_name is null)
AND (age = :age or :age is null)
AND (sex = :sex or :sex is null)

لینک سوال

اگر پارامتر null را به موارد رها شده بدهیم دیگر جای نگرانی نیست. البته در این حالت اگر از PDO استفاده می کنید باید emulation mode روی حالت ON باشد. در واقع با نگاه به کد بالا متوجه می شوید که تنها کافی است متغیر هایمان را به placeholder ها bind کنیم (یا مقداری دارند و یا null می شوند). اگر چنین کاری انجام دهیم، آن هایی که مقادیر null دارند دور انداخته می شوند و فقط آن هایی که مقدار مشخصی دارند درون کوئری قرار می گیرند.

به هر حال همیشه به یاد داشته باشید که کوئری نهایی باید تنها از طریق دو منبع ساخته شود: قسمت های ثابت (constant) و یا placeholder ها. بنابراین می توان به شکل خلاصه گفت هر کوئری SQL تنها می تواند از دو نوع داده ساخته شود:

  • قسمت های ثابتی که به صورت دستی در اسکریپت نوشته شده اند.
  • استفاده از placeholder ها برای مقادیر پویا

اگر از این قانون پیروی کنید در مقابل تزریق SQL امن خواهید بود.

اشتباهات رایج

چند مورد از اشتباهات رایج برنامه نویسان در این زمینه از قرار زیر اند:

  • escape کردن داده های کاربر: این مورد از بزرگترین اشتباهات برنامه نویسان است. escape کردن داده های کاربر دو مشکل دارد
    • escape کردن: escape کردن تنها قسمتی از محافظت را برای یک نوع literal انجام می دهد و شما تنها با انجام آن، از تزریق SQL محافظت نمی شوید، بلکه اگر در جای درست از آن استفاده نکنید به برنامه تان صدمه می زنید.
    • داده ها یا ورودی های کاربر (user input): هر متغیری که وجود دارد می تواند خطرناک باشد و مهم نیست از سمت کاربر بیاید یا خیر. بنابراین هر متغیری باید قبل از اینکه در کوئری قرار بگیرد قالب بندی شود، چه از سمت کاربر باشد و چه از جایی دیگر.
  • magic quotes : هیچ گاه از این ویژگی استفاده نکنید. این ویژگی دقیقا پیاده سازی اشتباهات بالا (escape کردن داده های کاربر) بود که خوشبختانه در حال حاضر از زبان SQL حذف شده است. اگر هم نمی دانید این ویژگی چیست، چه بهتر! وقت خود را تلف نکنید!
  • اعتبار سنجی داده ها: اعتبار سنجی داده ها در فرم ها هیچ ربطی به SQL و امنیت پایگاه داده ی ما ندارند. در واقع موارد پایه ای را می توانیم کنترل کنیم اما مطمئن باشید که اعتبار سنجی فرم ها نمی تواند شما را از خطر تزریق SQL در امان نگه دارد. آیا مثال خانم Sarah O'Hara را از جلسات قبل به یاد دارید؟ این نام کاملا صحیح و معتبر است و هنگام اعتبار سنجی هم معتبر جلوه می کند اما باعث بروز مشکل در برنامه ی ما شد.
  • htmlspecialchars (همچنین مواردی مانند ()filter_var و ()strip_tags): از اسم این مورد پیداست! اسمش HTML است یعنی مربوط به SQL نیست و شما نباید آن را به تزریق SQL ربط دهید. تمام این مواردی که من ذکر می کنم کارایی های خاص خودشان را دارند و اینطور نیست که بگویم بی استفاده هستند، بلکه می گویم در زمینه ی محافظت در برابر تزریق SQL بی اهمیت هستند. قالب بندی SQL هیچ گاه نباید داده ها را تغییر دهد! مثلا زمانی که جواهرات خود را داخل گاوصندوق می گذارید تا از آن ها محافظت کنید انتظار دارید بعدا همان جواهرات را دست نخورده بردارید نه اینکه قسمتی از آن تغییر کرده باشد! در زبان SQL نیز همین مسئله برقرار است؛ کار پایگاه داده ذخیره سازی داده است نه محافظت کردن و تغییر دادن آن.
  • استفاده از یک تابع برای sanitization تمام داده ها: داده های ما در زمینه های مختلفی استفاده می شوند (کوئری های SQL، کد های HTML، کد های JS، کد های JSON و ...). به همین دلیل نمی توانیم همه را به یک شکل پاک سازی کنیم. شما باید داده هایتان را برای هر زمینه ی خاص به طور جداگانه پاک سازی کنید تا مشکلی در برنامه تان پیش نیاید.
  • استفاده از پایگاه های داده ی جداگانه برای اجرای کوئری های DML: این مورد هم هیچ کمکی به شما نمی کند. در واقع این روش سعی میکند اگر پایگاه داده ی ما هک شد و عملیات SQL Injection انجام شد، اطلاعات ما به دست افراد ناشناس نیوفتد. بنابراین حفاظت در برابر SQL Injection محسوب نمی شود (به علاوه ی اینکه ممکن است هزینه ها و زمان ما را بی دلیل مصرف کند).

اما قبل از پایان این دوره به برخی از سوالاتی که حس می کنم ممکن است در ذهنتان خطور کند پاسخ می دهم:

- این دوره بسیار کوتاه بود. چرا؟

پاسخ: این دوره مکمل دوره ی آموزشی PDO بود به همین دلیل کوتاه و مختصر از آب در آمد. اکثر موضوعات اصلی در آن دوره گفته شد و بازتوضیح آن ها در این دوره فقط تکرار مکررات محسوب می شد بنابراین به شما پیشنهاد می کنم حتما آن دوره را نیز چک کنید.


- چرا به طور خاص از PDO یا MYSQL استفاده نشد؟

پاسخ: در این دوره سعی کردیم توجه خود را روی مبحث SQL Injection (به عنوان یک مبحث فارغ از وسیله های مختلف) متمرکز کنیم تا مباحث کلی و قابل تعمیم باشند و شما بتوانید آن را در MySQL یا PDO یا هر روشی که دارید، پیاده کنید.

از شما تشکر می کنم که همراه بنده بودید و امیدوارم این دوره به شما و امنیت وب سایتتان کمک کرده باشد.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری مقابله با SQL Injection توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.