آموزش اصلاح کد و جلوگیری از بوجود آمدن باگ و رخنه امنیتی تزریق به دیتابیس یا پایگاه داده (SQL Injection) در برنامههای تحت وب با زبان PHP.
متخصری درباره SQL Injection:
حمله تزریق به دیتابیس یا تزریق به پایگاه داده (SQL Injection) نوعی از حملات تحت وب است که جزو سه رخنه امنیتی برتر سایت ها است. متاسفانه بسیاری از طراحان و توسعه دهندگان سایتها و زبانهای برنامه نویسی تحت وب، این مورد را نادیده گرفته یا به دلیل نا آگاه بودن درباره این حمله، دَرِ سرویس خود را برای حملههای گوناگون باز میکنند درحالی که میتوانند با انجام کارهای بسیار بسیار ساده از این باگ جلوگیری کنند.
در اس کیو ال اینجکشن هکر یا حمله کننده یک رخنه امنیتی در سایت پیدا میکند که با استفاده از آن میتواند اقدام به اجرای دستورات SQL دلخواه خود در سمت سرور کند. SQL زبان ارتباط با دیتابیس است و حمله کننده با استفاده از این متد میتواند به هر آن چه که در دیتابیس وجود دارد (از رمزهای عبور گرفته تا اطلاعات حساس بانکی) دسترسی داشته باشد، آنها را تغییر دهد یا رکوردهای جدیدی به دیتابیس اضافه کند.
برای اطلاعات بیشتر درباره این باگ، نحوه کارکرد و خطرات آن پست “تزریق به دیتابیس (SQL Injection) چیست؟” را بخوانید.
نحوه جلوگیری از باگ SQL Injection:
پیش نیاز این قسمت، درک نحوه کارکرد باگ SQL Injection است که در پست”تزریق به دیتابیس (SQL Injection) چیست؟” توضیح داده شده است.
حال که با این رخنه امنیتی آشنا شدیم، سراغ پچ و اصلاح کردن کدهایی که با PHP نوشتهایم، میرویم (برای ASP.NET این لینک را به زبان انگلیسی بخوانید). برای جلوگیری از بوجود آمدن باگ SQL Inejection، باید از Query های پارامتر شده استفاده کنید.
کوئریهای پارامتر بندی شده نوعی ارتباط با دیتابیس است که در آن کوئری (Query) ما یک بار بدون داشتن مقدار متغیر، به سرور دیتابیس ارسال شده و بعد از آن، متغیرها یکی یکی و به صورت جداگانه ارسال میشوند. این قابلیت مزایای مختلفی دارد از جمله:
- افزایش سرعت کار با دیتابیس: دستور SQL خود را یک بار معرفی کرده و به سرور ارسال میکنیم و بعد از آن هر چند دفعه که نیاز است، فقط متغیرها را ارسال میکنیم. با این کار هم تاخیر آماده کردن کوئری کامل کمتر میشود و هم پهنای باند استفاده شده چون کوئری تنها یک بار ارسال میشود، بسیار بهینه تر خواهد بود.
- افزایش امنیت: متغیرهای ما به صورت مستقیم در دستور sql مان قرار نگرفته و به صورت جداگانه ارسال میشود. بنابراین هکر قادر نخواهد بود به استیتمنت (همان کوئری) ما دستور دیگری اضافه کند و بدین ترتیب از باگ SQL Injection جلوگیری میشود.
در زبان PHP:
اگر از زبان پی اچ پی استفاده میکنید، یکی از کاربردی ترین روشها برای پارامتری کردن کوئریها استفاده از اکستنشن PDO (مخفف PHP Data Objects) است. با استفاده از این کلاس میتوانید به اکثر دیتابیسها بدون هیچ مشکل وصل شوید. البته از روشهای دیگر (مثل MySQLi) نیز میتوانید برای پارامتری کردن استفاده کنید ولی پیشنهاد میکنم PDO را انتخاب کنید چون علاوه بر سادگی، از دیتابیسهای زیادی پشتیبانی میکند.
اگر در هاست شما PDO نصب نیست، به این لینک بروید. آموزش نصب کردن آن از بحث این پست خارج است.
ابتدا متغیرهای خود را تعریف میکنیم:
// نام کاربری یوزر دیتابیس
$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();
}
امنیت اضافی
- نسخههای مختلف دیتابیسها (مثلاً MySQL های قدیمی) دارای مشکلات امنیتی مختلفی هستند. همیشه برنامه دیتابیس خود به روز نگه دارید.
- همچنین زبانهای برنامه نویسی PHP و ASP.NET پچهای امنیتی برای باگهای خود منتشر میکنند، پس همیشه از آخرین نسخه استفاده کنید.
- پس از اتمام کار برنامه نویسی، خاصیت نشان دادن خطاهای دیتابیس را غیرفعال کنید یا حداقل آنها را به کاربر نشان ندهید.
- یوزری که از آن برای دسترسی به دیتابیس استفاده میکنید را محدود کنید!
- اطلاعات حساس مقایساتی (مانند رمزهای عبور) را به صورت هش شده با الگوریتمهای قوی، سالت (Salt) شده و کند ذخیره کنید تا حتی درصورت از دست رفتن، پیدا کردنشان دشوار باشد.
در نهایت …
اگر از نسخههای جدید تر MySQL (مانند ۵.۱ و ۵.۵ و ۵.۶ و …) و PDO با DSN کاراکتر ست (در PHP نسخه ۵.۳.۶ و بالاتر) استفاده میکنید، و از طرفی کاراکتر ست دیتابیس خودتان را به یک کاراکتر ست امن (مانند UTF-8) تنظیم کنید و البته همه متغیرهای ورودی کاربران را با استفاده از PDO بصورت پارامتر شده به سرور دیتابیس ارسال کنید، سرویس شما به شرط این که در همه جا از این دستورات پارامتر شده استفاده کنید، اگر در مقابل باگ SQL Injection تا ۱۰۰% امن نباشد، حداقل ۹۹% امن خواهد شد. چون امنیت در هیچ چیزی ۱۰۰% نیست!
سلام عزیزم 🙂
از اینکه پیشرفت کردی خوشحالم 🙂
دنبال یه مقاله میگشتم که توی سایتت پیدا کردم
ایشالا که پیشرفت های زیادی بکنی :))
زنده باشید دوست عزیز 🙂 خیلی ممنون.
سلام عالی بود
فقط میشه همین کارو با mysqli هم بگین خیلی ممنون میشم
با سپاس…
سلام. در mysqli میتونید اینجوری پارامتر بندی کنید کوئری رو :
$stmt = $dbConnection->prepare(‘SELECT * FROM employees WHERE name = ?’);
$stmt->bind_param(‘s’, $name);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// do something with $row
}