ناتوانی prepared statement ها در identifier ها

16 اسفند 1397
درسنامه درس 4 از سری مقابله با SQL Injection
SQL-Injection-preprade-statement

با سلام و احترام خدمت شما خوانندگان گرامی، در قسمت قبل با مبحث prepared statement ها آشنا شدیم و با کمک آن ها از شر قالب بندی دستی خلاصی پیدا کردیم اما هنوز یک مبحث در مورد prepared statement ها باقی مانده است: آیا می توان از آن ها برای محافظت از identifier ها نیز استفاده کرد؟ آیا در هر موردی می توان از آن ها استفاده کرد؟ امروز می خواهیم در مورد ضعف prepared statement ها صحبت کنیم.

ناتوانی prepared statement ها

در جلسه ی قبل با مثال های ساده و مبتدی آشنا شدیم. با اینکه آن مثال های ساده در دنیای واقعی پرکاربرد هستند اما بخواهیم یا نخواهیم، کوئری های پیشرفته تری نیز خواهیم نوشت. کوئری هایی که پویا تر و پیچیده تر هستند و شامل identifier و یا هر ساختار پیچیده تری مانند آرایه ها نیز می باشند.

متاسفانه برای حفاظت از identifier ها در مقابل حملات تزریق SQL هیچ راهی به جز همان قالب بندی دستی وجود ندارد:

$field = "`".str_replace("`","``",$field)."`";
$sql   = "SELECT * FROM t ORDER BY $field";
$data  = $db->query($sql)->fetchAll();

برای آرایه ها نیز به این شکل عمل می کنیم تا کوئری ساخته شود:

$ids = array(1,2,3);
$in  = str_repeat('?,', count($arr) - 1) . '?';
$sql = "SELECT * FROM table WHERE column IN ($in) AND category=?";
$stm = $db->prepare($sql);
$ids[] = $category; //adding another member to array
$stm->execute($ids);
$data = $stm->fetchAll();

در واقع می توان گفت برای قالب بندی identifier ها در MySQL باید پیروی دو قانون زیر باشید:

  • identifier ها را بین دو ` قرار دهید (نام این علامت backtick است).
  • backtick ها را با دوبرابر کردنشان، escape کنید.

به طور مثال به این کوئری نگاه کنید:

$table = "`".str_replace("`","``",$table)."`";

این کد ابتدا در نام جدول علامت ` را پیدا کرده و سپس آن را دو برابر می کند (یعنی ``) سپس کل این رشته را داخل دو علامت ` می گذارد. بعد از انجام چنین قالب بندی، وارد کردن table$ در کوئری کاملا بی خطر است. این قوانین برای پایگاه های داده ی دیگر متفاوت هستند اما نکته ای که شما باید در ذهن داشته باشید این است که جداکننده ها به تنهایی کافی نیستند و خود آن ها را نیز باید escape کرد.

در واقع سعی شما باید بر این باشد که identifier های پویا را با لیستی از مقادیر مجاز چک کنید:

$orders  = ["name","price","qty"]; //نام فیلد
$key     = array_search($_GET['sort'],$orders); // ای موجود است؟ name چک کنید که آیا چنین 
$orderby = $orders[$key]; //اگر وجود نداشت اولی به صورت خودکار انتخاب می شود
$query   = "SELECT * FROM `table` ORDER BY $orderby"; //مقدار ما امن است

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

همین روش را می توانیم برای دستورات INSERT و UPDATE نیز انجام دهیم چرا که MySQL از دستور SET برای هر دوی این موارد پشتیبانی می کند:

$data = ['name' => 'foo', 'submit' => 'submit']; // data for insert
$allowed = ["name", "surname", "email"]; // allowed fields
$values = [];
$set = "";
foreach ($allowed as $field) {
    if (isset($data[$field])) {
        $set.="`".str_replace("`", "``", $field)."`". "=:$field, ";
        $values[$field] = $data[$field];
    }
}
$set = substr($set, 0, -2);

در واقع دستور بالا ترتیب مناسب را برای اپراتور SET ایجاد می کند که فقط دارای فیلد ها و placeholder های مجاز هستند مانند مثال زیر:

`name`=:foo

برای مقابله با این مشکلات کتابخانه های مختلفی در گیت هاب ارائه شده اند که می توانید از آن ها استفاده کنید. برخی از این کتابخانه ها عبارتند از:

شما می توانید با مراجعه به صفحات این کتابخانه ها در گیت هاب، بر اساس نیاز خود کتابخانه های خود را انتخاب کنید و از آن ها در پروژه هایتان استفاده کنید. طرز کار این کتابخانه ها به زبان ساده به این شکل است که placeholder ها را به همراه نوعشان می آورند. به طور مثال s% به معنی رشته (String) یا  d% به معنی عدد (digit) می باشد. اگر به صفحه ی آن ها بروید به سادگی به طرز کارشان پی می برید. همه ی آن ها در صفحه شان توضیح کار با کتابخانه را آورده اند.

prepared statement ها بسیار مفید هستند اما در همه جا کافی نیستند همانطور که می بینید در مورد identifier ها جواب گو نبودند. دو مورد کلی وجود دارد که  prepared statement ها در آن ها یا عملی نیستند و یا کافی نیستند:

  • اگر از جلسه ی قوانین قالب بندی به یاد داشته باشید هیچ راهی برای استفاده از prepared statement در کلیدواژه های SQL نداریم.
  • زمانی که لیستی از identifier های پویا داشته باشیم که از کاربر دریافت می کنیم اما فیلدهایی نیز باشند که کاربر اجازه ی دسترسی به آن ها را نداشته باشد. یکی از حالت های معمول این است که مقادیر key و value را از آرایه ی POST_$ را دریافت می کنیم و با اینکه می توانیم هر دو را قالب بندی کنیم اما فیلد هایی مانند admin و permissions نیز وجود دارند که تنها باید توسط ادمین سایت تنظیم شوند.

برای حل هر دو مورد باید به روشی که بالاتر گفته شد عمل کنیم یعنی whitelisting. در این روش، پارامتر های پویا را از قبل در اسکریپت خود می نویسید و تمام مقادیر مجاز باید از آن ها انتخاب شوند. به طور مثال برای یک سفارش می توانیم اینطور بنویسیم:

$orders  = array("name","price","qty"); //نام فیلد ها
$key     = array_search($_GET['sort'],$orders)); // چک می کنیم تا ببینیم چنین نامی داریم یا خیر
$orderby = $orders[$key]; //اگر چنین نامی نداریم، اولی به صورت خودکار تنظیم می شود
$query   = "SELECT * FROM `table` ORDER BY $orderby"; //مقدار ما امن است

و برای کلیدواژه های SQL نیز راهی به جز whitelisting نداریم چرا که هیچ قالب بندی برایشان ممکن نیست بنابراین:

$dir = $_GET['dir'] == 'DESC' ? 'DESC' : 'ASC'; 
$sql = "SELECT * FROM t ORDER BY field $dir"; //مقدار ما امن است

مثلا در کتابخانه ی SafeMysql که بالاتر آن را معرفی کردیم دو تابع برای whitelisting ارائه داده است؛ یکی به شکل آرایه های key=>value و دیگری برای کلیدواژه ها به صورت تنها.

امیدوارم از این قسمت لذت برده باشید. به قسمت های پایانی این سری آموزشی نزدیک می شویم. در چند قسمت آینده در رابطه با برخی از نکات باقی مانده و روش های بد برنامه نویسی صحبت کرده و دوره را تمام می کنیم.

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

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