SQL Injection (تزریق به دیتابیس):
تزریق به پایگاه داده یا دیتابیس (SQL Injection) نوعی از حملات وب است که در آن فرد حمله کننده یا هکر میتواند اقدام به اجرا کردن دستورات دلخواه و مخرب خود بر روی پایگاه داده وب سایت مورد هدف کند. در این حمله، حمله کننده با استفاده از دانش خود (یا تنها با استفاده از یک برنامه ساده!) میتواند از نقضهای امنیتی موجود در کدهای نوشته شده توسط برنامه نویس سایت استفاده کرده و به اصطلاح آنها را اکسپلویت کند. چون در این حمله هکر درواقع به کد اسکیوال، کد دلخواه خود را اضافه میکند، تزریق SQL نام گرفته است.
یک حمله موفق اس کیو ال اینجکشن میتواند به راحتی سبب افشای دادههای مهم در دیتابیس (ازجمله رمزهای عبور، اطلاعات فردی کاربران و ...)، اضافه کردن دادههای دلخواه حمله کننده به دیتابیس یا حذف کردن دادههای خاص از دیتابیس گردد.
حمله SQL Injection چگونه کار میکند؟
فرض کنید شما یک برنامه نویس سمت وب هستید و قصد دارید با دیتابیس از طریق دستورات SQL ارتباط برقرار کنید. چون همانطور که میدانید استفاده از زبان SQL برای برقراری ارتباط با دیتابیس ضروری است. کد زیر را فرض کنید که در زبان PHP نوشته شده است:
$sql_statement = "SELECT * FROM users WHERE name = '" . $_GET["name"] . "'";
دستور بالا مقدار فیلد یا همان پارامتر username که توسط متد GET، متدی که پارامترها در مسیر URL مشخص میشوند، از طرف کاربر به سرور ارسال شده را در بین دو علامت ' قرار میدهد (تا توسط سرور SQL به عنوان رشته حروفی شناخته شود) و پس از آن فرض کنید که مقدار موجود در متغیر $sql_statement قرار است اجرا شود.
فرض کنید که کاربر فیلد name را به صورت عادی مثل مقدار زیر پر کند. مسیر از طریق متد GET به این شکل خواهد شد:
http://example.com/vul.php?name=Amirreza
در این صورت Query موجود در متغیر sql_statement به این صورت خواهد بود:
$sql_statement = "SELECT * FROM users WHERE name = 'Amirreza'";
و کوئری (حرف همزه به دلیل فونت سایت مانند ن نشان داده میشود) بدون مشکل اجرا خواهد شد. در دیتابیس فیلد name با رشته Amirreza مقایسه خواهد شد و در جایی که نتیجه یافت شد، همه فیلدها (*) برای بقیه کدها در دسترس خواهد بود.
حال فرض کنید که کاربر (در اینجا، حمله کننده) فیلد name را به صورت زیر پر کند:
http://example.com/vul.php?name=' UNION UPDATE users SET email = 'hacker@example.com' WHERE name = 'admin'
به یک نام کاربری شبیه نیست، درست است؟ خب حال این مقدار را جایگزین پارامتر username متد POST میکنیم. متغیر کوئری ما به این شکل خواهد بود:
$sql_statement = "SELECT * FROM users WHERE name = '' UNION UPDATE users SET email = 'hacker@example.com' WHERE name = 'admin'";
خب کوئری بالا هم اجرا خواهد شد ولی با این تفاوت که در واقع هکر کوئری دلخواه خود را اجرا کرده است! در دیتابیس فیلد name با رشته خالی جستجو خواهد شد ولی چیزی پیدا نخواهد شد، تا اینجا برای هکر مهم نیست. اما هکر با استفاده از دستور UNION، کوئری دوم خود را نیز اجرا میکند و در کوئری دوم، ایمیل کاربر admin (که به احتمال زیاد تمامی دسترسیهای موجود را دارد) را به ایمیل دلخواه تغییر (UPDATE) میدهد.
چگونه از باگ SQL Injection جلوگیری کنیم؟
نحوه جلوگیری از باگ SQL Injection نیاز به آگاهی برنامه نویس تحت وب از نحوه عملکرد این باگ دارد و جلوگیری از آن بسیار ساده تر از چیزی است که معمولاً تصور میشود.
نحوه جلوگیری از باگ SQL Injection:
کوئریهای پارامتر بندی شده نوعی ارتباط با دیتابیس است که در آن کوئری (Query) ما یک بار بدون داشتن مقدار متغیر، به سرور دیتابیس ارسال شده و بعد از آن، متغیرها یکی یکی و به صورت جداگانه ارسال میشوند. این قابلیت مزایای مختلفی دارد از جمله:
§ افزایش سرعت کار با دیتابیس: دستور SQL خود را یک بار معرفی کرده و به سرور ارسال میکنیم و بعد از آن هر چند دفعه که نیاز است، فقط متغیرها را ارسال میکنیم. با این کار هم تاخیر آماده کردن کوئری کامل کمتر میشود و هم پهنای باند استفاده شده چون کوئری تنها یک بار ارسال میشود، بسیار بهینه تر خواهد بود.
§ افزایش امنیت: متغیرهای ما به صورت مستقیم در دستور sql مان قرار نگرفته و به صورت جداگانه ارسال میشود. بنابراین هکر قادر نخواهد بود به استیتمنت (همان کوئری) ما دستور دیگری اضافه کند و بدین ترتیب از باگ SQL Injection جلوگیری میشود.
در زبان PHP:
اگر از زبان پی اچ پی استفاده میکنید، یکی از کاربردی ترین روشها برای پارامتری کردن کوئریها استفاده از اکستنشن PDO (مخفف PHP Data Objects) است. با استفاده از این کلاس میتوانید به اکثر دیتابیسها بدون هیچ مشکل وصل شوید. البته از روشهای دیگر (مثل MySQL) نیز میتوانید برای پارامتری کردن استفاده کنید ولی پیشنهاد میکنم PDO را انتخاب کنید چون علاوه بر سادگی، از دیتابیسهای زیادی پشتیبانی میکند.
ابتدا متغیرهای خود را تعریف میکنیم:
حال نوبت به تعریف آبجکت 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) شده و کند ذخیره کنید تا حتی درصورت از دست رفتن، پیدا کردنشان دشوار باشد.
- ۹۵/۰۳/۱۱