Prepared statements و مقابله با SQL Injection

11 اسفند 1397
درسنامه درس 3 از سری مقابله با SQL Injection
sql-injection-prepared-statements

سلام و احترام خدمت شما همراهان روکسو! در جلسه ی قبل از دوره آموزشی مقابله با SQL Injection به بحث قوانین قالب بندی و قالب بندی دستی اشاره کردیم و متوجه شدیم که قالب بندی دستی اشکالات زیادی به همراه دارد اما به راه حلی نرسیدیم. در این قسمت قصد داریم راه حل آن را ارائه کنیم: Prepared statement ها!

Prepared statement چیست؟

مفهوم prepared statement ها به زبان ساده از این قرار است:

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

در مثال زیر ما داده ها را به صورت مستقیم و از طریق یک متغیر وارد پایگاه داده می کنیم:

uName = getRequestString("username");
uPass = getRequestString("userpassword");
 
sql = 'SELECT * FROM Users WHERE Name ="' + uName + '" AND Pass ="' + uPass + '"'

اگر کاربر صادقانه فرم را پر کند مثالی شبیه به حالت زیر پیش می آید:

SELECT * FROM Users WHERE Name ="John Doe" AND Pass ="myPass"

اما اگر خرابکارانه کدی مثل کد زیر را وارد کند اوضاع متفاوت می شود:

SELECT * FROM Users WHERE Name ="" or ""="" AND Pass ="" or ""=""

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

این مفهوم کلی و ساده ی SQL Injection است. برای آشنایی بیشتر با این بحث به این مقاله که قبلا نوشته شده است مراجعه کنید.

قبل از هر چیز دوست دارم متوجه تفاوت دو موضوع اصلی بشوید:

  • قابلیت استفاده از prepared statement ها به صورت Native در DBMS های مطرح دنیا.
  • مفهوم استفاده از placeholder ها به عنوان نماینده ی داده ها در یک کوئری.

قابلیت استفاده از prepared statement ها به شکل Native بسیار ساده و هوشمندانه است؛ کوئری و داده های اصلی اش به صورت جداگانه به سمت سرور ارسال می شوند و احتمال ندارد که بین‌شان تداخلی به وجود بیاید بنابراین SQL Injection غیر ممکن می شود. البته باید گفت که این نوعِ استفاده محدودیت های خود را نیز دارد چرا که تنها از دو نوع literal پشتیبانی می کنند (رشته ها و اعداد) و برای استفاده در دنیای واقعی کافی نیستند.

در مورد native prepared statement ها باورهای غلطی وجود دارد:

  • سریع تر هستند: در PHP اینطور نیست! چرا که PHP به شما اجازه نمی دهد از یک prepared statement در فراخوانی های مختلف استفاده ی دوباره کنید و از طرفی کوئری های تکراری که در یک instance باشند نیز به ندرت اتفاق می افتند بنابراین سرعتی در کار نیست.
  • امن تر هستند: این گفته صحیح است اما نه به خاطر Native بودنشان بلکه به این دلیل که prepared statement هستند و از قالب بندی دستی استفاده نمی کنند.

از اول مقاله نیز بحث ما همین بوده است؛ ساختن یک کوئری با قسمت های ثابت و placeholder هایی که بعدا جایشان را به داده های اصلی می دهند! این نوع کوئری ها به صورت خودکار قالب بندی می شوند.

بهترین و برترین مزیت استفاده از prepared statement ها حذف خطر تزریق SQL است:

  • یک prepared statement تمام مسائل قالب بندی را بر عهده می گیرد و به صورت خودکار انجامشان می دهد بدون اینکه نیازی به دخالت برنامه نویس داشته باشد.
  • یک prepared statement فرمت بندی را بدون هیچ نقصی انجام می دهد و مطمئن است.
  • یک prepared statement فرمت بندی را دقیقا در زمان درست آن، یعنی دقیقا قبل از اجرای کوئری، انجام می دهد.

در واقع دلیل تنفر و دوری از قالب بندی دستی و تعریف و تمجید از prepared statement ها همین موارد است. استفاده از prepared statement ها دو مزیت دیگر نیز دارد که حیاتی نیستند اما باز هم کمک خوبی هستند:

  • prepared statement ها داده اصلی را دستکاری نمی کنند بنابراین می توان از داده ی اصلی در جای دیگر استفاده کرد؛ مثلا می توان آن را در مرورگر نمایش داد یا در کوکی ها ذخیره کرد.
  • در برخی از موارد برنامه نویس ها می توانند با استفاده از prepared statement ها کد هایشان را بسیار کوتاه تر کنند.

بنابراین Native بودن یک prepared statement حیاتی نیست، همانطور که در سری آموزش PDO دیدیم، می توانیم prepared statement ها را emulate (شبیه سازی) کنیم و با روشن بودن PDO::ATTR_EMULATE_PREPARES (تنظیم روی true) کوئری را یکجا به سرور بفرستیم. در این حالت داده ها باز هم به صورت صحیح قالب بندی می شوند و می توان گفت که امن است.

حتی با اکستنشن (افزونه) قدیمی MySQL نیز می توان از prepared statement ها استفاده کرد. تابع زیر می تواند با استفاده از extension مذکور نهایت امنیت را به شما بدهد:

function paraQuery()
{
    $args  = func_get_args();
    $query = array_shift($args);
    $query = str_replace("%s","'%s'",$query); 

    foreach ($args as $key => $val)
    {
        $args[$key] = mysql_real_escape_string($val);
    }

    $query  = vsprintf($query, $args);
    $result = mysql_query($query);
    if (!$result)
    {
        throw new Exception(mysql_error()." [$query]");
    }
    return $result;
}

$query  = "SELECT * FROM table where a=%s AND b LIKE %s LIMIT %d";
$result = paraQuery($query, $a, "%$b%", $limit);

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

تمام مقادیر پویا و دینامیک (یعنی مقادیری که تغییر می کنند، مانند متغیر ها) باید با prepared statement ها وارد کوئری شوند. چرا مقادیر پویا؟ به این دلیل که قسمت های ثابت یک کوئری هیچ وقت تغییر نمی کنند و هر مشکلی در این زمینه در هنگام توسعه و کدنویسی وب سایت مشخص خواهد شد. بنابراین نیازی به دستکاری قسمت های ثابت وجود ندارد. در مثال بالا SELECT و FROM ثابت هستند. آیا می توانید حالتی را تصور کنید که باعث مشکل شوند؟ خیر!

هدف من از پیش کشیدن این بحث چه بود؟

هدف من این بود که متوجه شوید امنیت اصلی متعلق به بحث prepared statement ها است و مخصوص PDO نیست اما پیشنهاد ما استفاده از PDO است. می توانید به دوره ی آموزش PDO در همین سایت مراجعه کنید.

بحث را در این قسمت خاتمه می دهم. در قسمت بعد در مورد identifier ها و امنیت آن ها صحبت خواهیم کرد.

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

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

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