جلوگیری از حمله تزریق به پایگاه داده (SQL Injection)
در مقاله پیشین در مورد حمله تزریق به دیتابیس توضیحاتی داده شد. اینک در این مقاله قصد داریم به مواردی که میتواند از بروز این حمله جلوگیری کرده و امنیت وب سایت را افزایش دهد اشاره نماییم.
حمله تزریق به دیتابیس یا پایگاه داده (SQL Injection) نوعی از حملات تحت وب است که به علت اشتباهات برنامه نویسان در طراحی و توسعه سایت رخ میدهد و جزو سه رخنه امنیتی برتر سایت ها است. در این حمله هکر با پیدا کردن یک مشکل امنیتی در سایت دستورات SQL مورد نظر خود را در سمت سرور اس کیو ال اجرا خواهد کرد. برنامه نویسان میتوانند با پیکربندی موارد بسیار ساده از این باگ امنیت جلوگیری نمایند. با این حمله هکر خواهد توانست به دیتابیس دسترسی داشته و تمام اطلاعات حساس مانند رمزهای عبور کاربران را تغییر داده و یا حذف نماید.
چگونه از باگ SQL Injection جلوگیری کنیم؟
برای جلوگیری از باگ تزریق به دیتابیس ، نیاز است تا برنامه نویس سایت از نحوه عملکرد این باگ آگاهی داشته باشد. بنابراین به سراغ کدهایی که نوشته ایم رفته و از کوئری های پارامتریندی شده استفاده میکنیم.
کوئریهای پارامتر بندی شده نوعی ارتباط با دیتابیس است که در آن کوئری (Query) ما یک بار بدون داشتن مقدار متغیر، به سرور دیتابیس ارسال شده و بعد از آن، متغیرها یکی یکی و به صورت جداگانه ارسال میشوند. این قابلیت مزایای مختلفی دارد از جمله:
- افزایش سرعت کار با دیتابیس: دستور SQL خود را یک بار معرفی کرده و به سرور ارسال میکنیم و بعد از آن هر چند دفعه که نیاز است، فقط متغیرها را ارسال میکنیم. با این کار هم تاخیر آماده کردن کوئری کامل کمتر میشود و هم پهنای باند استفاده شده چون کوئری تنها یک بار ارسال میشود، بسیار بهینه تر خواهد بود.
- افزایش امنیت: متغیرهای ما به صورت مستقیم در دستور sql مان قرار نگرفته و به صورت جداگانه ارسال میشود. بنابراین هکر قادر نخواهد بود به استیتمنت (همان کوئری) ما دستور دیگری اضافه کند و بدین ترتیب از باگ SQL Injection جلوگیری میشود.
در صورتیکه از زبان پی اچ پی استفاده میکنید، یکی از کاربردی ترین روشها برای پارامتری کردن کوئریها استفاده از افزونه PDO (PHP Data Objects ) است. با استفاده از این ابزار میتوانید به اکثر دیتابیسها بدون هیچ مشکلی متصل شوید و دیتابیس را به گونه ای طراحی کنید که هر زمانی میتوانید نوع آن را تغییر دهید.
ابتدا متغیرهای خود را تعریف میکنیم:
نام کاربری یوزر دیتابیس
$username = "root";
پسورد یوزر دیتابیس
$password = "";
هاست دیتابیس
$host = "localhost";
نام دیتابیس
$dbname = "my_database";
حال باید ابجکت PDO را تعریف کنیم.این کار را در بلوک Try - Catch انجام میدهیم تا در صورت وجود مشکل، ما را مطلع کند:
try {
// آبجکت پی دی او خود را در زیر تعریف میکنیم
$db = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password, $options);
} catch(PDOException $ex) {
// اگر مشکلی در ارتباط با دیتابیس پیش آمد
die("Failed to connect to the database: " . $ex->getMessage());
}
در $db آبجکت PDO ما با DSN ای که داخل آن نوشته شده، تعریف شده است.
در این DSN از charset=utf8 استفاده کردهایم. با این کار دیتابیس متوجه خواهد شد که تنها کاراکترهایی با اینکدینگ یا کاراکتر ست UTF-8 را قبول کند. این مورد در جلوگیری از SQL Injection بسیار کاربردی است زیرا کاراکترهای خارج از UTF-8 در سمت دیتابیس قبول نخواهند شد.
نکته: اگر PDO شما از charset در DSN خود پشتیبانی نکند، باید کاراکتر ست را به صورت دستی ست کنید. در این حالت، کافیست قبل از بلوک Try - Catch دستور زیر را بنویسید که آرایهای از تنظیمات اختیاری برای PDO میسازیم که میتواند شامل بیش از یک تنظیم باشد. در این آرایه، با دستور SET NAMES utf8 کاراکتر ست را انتخاب میکنیم:
$options = array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8');
توجه کنید که تنها اگر از charset پشتیبانی نکند از دستور بالا استفاده کنید!
حال برخی از خواص $db خود که شامل یک آبجکت PDO است را مشخص میکنیم. ابتدا حالت نشان دادن خطا را فعال میکنیم تا در قسمتی از کدها اگر مشکلی بود، بتوانیم در بلوک Try - Catch آن را مدیریت کنیم:
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
توجه کنید که پس از اتمام برنامه نویسی وب، شدیداً توصیه میشود که حالت نشان دادن خطا را غیرفعال کنید تا هکرها در صورت وجود باگ از آن مطلع نشوند.
حال خصوصیت نحوه بازگرداندن دادهها را انتخاب میکنیم. در این قسمت تعریف میکنیم که دادهها را به صورت آرایهای با نام ستون جدول دیتابیس نشان دهد یعنی مثلاً اگر ستونی با نام user_lastname را درخواست کنیم، آن را میتوانیم در آرایهای با همین نام دریافت کنیم:
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
حال به مهمترین خصوصیت PDO رسیدهایم. ممکن است فرض کنید که PDO همیشه دادهها را به صورت پارامتر شده به دیتابیس ارسال میکند اما این تصور اشتباه است، ممکن است از طرف دیتابیس این قابلیت تایید نشود بنابراین PDO کوئری ما را به صورت از پیش آماده شده ارسال خواهد کرد که باعث بوجود آمدن باگ SQL Injection خواهد شد. با ست کردن خصوصیت زیر به PDO اطلاع میدهیم که همیشه از عمل پارامتر کردن استفاده کند:
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
با این حال باز هم امکان این وجود دارد که PDO در برخی از دیتابیسها، از حالت پارامتر شده استفاده نکند. تنها مزیت MySQLi نسبت به PDO این است که در MySQLi همیشه و همیشه کوئریها بصورت پارامتر شده ارسال میشوند. با این حال PDO مزیتهای بیشتری نسبت به MySQLi داشته و پارامتری کردن آن با دستور بالا و استفاده از نسخه دیتابیس هماهنگ، تضمین خواهد شد.
بنابراین پس از اینکه PDO تنظیم و آماده استفاده شد، با مثالی نحوه کار PDO را شرح میدهیم.
ابتدا از ترای - کچ برای مدیریت خطاها استفاده میکنیم:
try {
//کوئری خود را ابتدا به دیتابیس ارسال میکنیم
$stmt = $db->prepare("SELECT name, lastname FROM users WHERE key = :getkey AND username = :getusername);
//پارامترهای دلخواه خود را بایند میکنیم یا میتوانیم به صورت مستقیم در اجرا، آنها را وارد کنیم
$stmt->bindParam(':getkey', $key, PDO::PARAM_INT);
با ست کردن آرگومنت سوم، نوع داده را مشخص میکنیم. در اینجا، عدد صحیح
$stmt->bindParam(':getusername', $_POST[""]);
//اگر نوع را مشخص نکنیم، به صورت پیشفرض رشته حروفی است.
//حال کوئری و پارامترها را جداگانه به سرور میفرستیم.
$stmt->execute();
//دادههای گرفته شده از دیتابیس را ذخیره میکنیم
$rowF = $stmt->fetchAll();
if ($rowF){
// اگر رکوردی پیدا شد
foreach ($rowF as $row){
//برای هر رکورد، عملی را انجام میدهیم
echo "Your name is: " . $row["name"] . " and your last name is: " . $row["lastname"];
}
}else{
// اگر رکوردی پیدا نشد
echo "No records found with the Key and Username.";
}
}catch(PDOException $e){
// اگر خطایی بوجود آمد. پس از اتمام برنامه نویسی حتماً این خطا را از دید کاربر مخفی کنید
echo "ERROR: " . $query . "<br>" . $e->getMessage();
}
روش های مقابله
۱. بهترین راه مقابله با حملات SQL Injection چک کردن ورودی ها قبل ارسال آنها به پایگاه داده به عنوان یک Query است. میتوانید ورودی فرم ها را به کمک روشهای مختلف چک کنید که حاوی کارکترهای غیرمجاز نباشند.
۲. پیامهای خطایی که ایجاد میکنید با دقت بیشتری انتخاب کنید.مثلاً خطای ‘ نام کاربری نمیتواند شامل اعداد باشد’ را که توسط پایگاه داده برگردانده میشود در نظر بگیرید. همین اطلاعات کم می تواند به مهاجم این کمک را بکند که در قسمت نام کاربری اعداد را وارد نکند.
۳. نسخههای مختلف دیتابیسها (مثلاً MySQL های قدیمی ) دارای مشکلات امنیتی مختلفی هستند. همواره برنامه دیتابیس خود را آپدیت نمایید.
۴. زبانهای برنامه نویسی PHP و ASP.NET پچهای امنیتی برای باگهای خود منتشر میکنند، پس همیشه از آخرین نسخه استفاده کنید.
۵. پس از اتمام کار برنامه نویسی، خاصیت نشان دادن خطاهای دیتابیس را غیرفعال کنید یا حداقل آنها را به کاربر نشان ندهید.
۶. یوزری که از آن برای دسترسی به دیتابیس استفاده میکنید را محدود کنید.
۷. اطلاعات حساس (مانند رمزهای عبور) را به صورت هش شده با الگوریتمهای قوی، سالت (Salt) شده و کند ذخیره کنید تا حتی درصورت از دست رفتن، پیدا کردنشان دشوار باشد.
اگر از نسخههای جدید تر MySQL (مانند ۵.۱ و ۵.۵ و ۵.۶ و ...) و PDO با DSN کاراکتر ست (در PHP نسخه ۵.۳.۶ و بالاتر) استفاده میکنید، و کاراکتر ست دیتابیس خودتان را به یک کاراکتر ست امن (مانند UTF-8) تنظیم کرده و البته همه متغیرهای ورودی کاربران را با استفاده از PDO بصورت پارامتر شده به سرور دیتابیس ارسال نمایید، سرویس شما به شرط این که در همه جا از این دستورات پارامتر شده استفاده کنید، اگر در مقابل باگ SQL Injection تا ۱۰۰% امن نباشد، حداقل ۹۹% امن خواهد شد. چون امنیت در هیچ چیزی ۱۰۰% نیست!
- بازدید: 3959