چند نکته مهم درمورد SQL Injection و mysql_real_escape_string

من خودم چون از یک کلاس و تابعی که خودش این موارد رو هندل میکنه استفاده میکنم، دیگه این مسئله زیاد جلوی چشمم نیست و وقتی افراد از SQL Injection و mysql_real_escape_string صحبت میکنن حضور ذهن ندارم تا این نکته رو بهشون یادآوری کنم. ولی الان به ذهنم اومد و گفتم شاید خیلی مبتدی ها این قضیه رو نمیدونن یا به راحتی ازش غفلت میکنن.

اون نکته چیه؟

نکته اینه که تابع mysql_real_escape_string برای Escape کردن داده های رشته ای که موقع درج در کوئری توی کوتیشن میذاریم کارایی داره. اما درمورد داده های عددی که در کوتیشن نمیذاریم امنیت نمیده.

مثلا:

<?php

mysql_connect('', 'root', '');

$input='4 or 1=1';

echo mysql_real_escape_string($input);

?>

خروجیش:

4 or 1=1

فرض کنید متغییر input ورودی کاربر هست که حالا از طریق GET یا POST دریافت شده.
اینجا با وارد شدن عبارت 4 or 1=1 با اینکه ما اون رو از mysql_real_escape_string عبور دادیم اما مشاهده میکنید که رشتهء ورودی عملا هیچ تغییری نکرده و بنابراین اگر ما ورودی کاربر رو همینطور در یک کوئری ترکیب کنیم، عبارت وارد شده بعنوان دستورات SQL تفسیر و اجرا میشه.

مثلا کوئری:

select * from table1 where id=$input

عملا تبدیل میشه به:

select * from table1 where id=4 or 1=1

که مسلما اون چیزی نیست که ما میخوایم.

خلاصه این نکته رو باید همیشه مد نظر داشته باشید.

البته میتونید بجاش همیشه از یک تابعی مثل این استفاده کنید:

function quote_smart($value) {

if(is_numeric($value)) return $value;

if(get_magic_quotes_gpc()) $value = stripslashes($value);
return "'" .mysql_real_escape_string($value) . "'";

}

این تابع اول بررسی میکنه که آیا مقدار وارد شده یک عدد است و اگر عدد بود همونطور دست نخورده اون رو برمیگردونه، ولی اگر عدد نبود اون رو بوسیلهء mysql_real_escape_string اسکیپ میکنه. ضمنا دوتا کار مفید دیگه هم انجام میده:
1- اگر magic_quotes_gpc روشن باشه اثر اون رو خنثی میکنه تا رشتهء ورودی Escape مضاعف نشه.
2- بعد از Escape کردن ورودی اون رو داخل کوتیشن هم قرار میده و برمیگردونه؛ یعنی خروجی این تابع برای درج در کوئری آماده است و دیگه لازم نیست و نباید شما اون رو داخل کوتیشن بذارید.

حالا همون مثال رو با این تابع جدید بررسی میکنیم:

<?php

function quote_smart($value) {

if(is_numeric($value)) return $value;

if(get_magic_quotes_gpc()) $value = stripslashes($value);
return "'" .mysql_real_escape_string($value) . "'";

}

mysql_connect('', 'root', '');

$input='4 or 1=1';

echo quote_smart($input);

?>

خروجیش:

'4 or 1=1'

حالا مشاهده میفرمایید که درسته رشتهء ورودی ما تغییری نکرده، اما چون توسط این تابع در کوتیشن قرار گرفته دیگه بعنوان دستور SQL تفسیر نمیشه و نهایتا منجر به یک خطای سینتاکس MySQL میشه و نه چیز دیگه. یعنی خطری برای سایت نداره.

البته بنده نمیگم که این تابع لزوما برای هر کاری مناسبه و روشهای دیگر یا دستی خودتون رو بکار نبرید، ولی این تابع حداقل برای منکه کار رو خیلی راحت کرده تاحالا. ضمنا این تابع رو خودم ننوشتم و از منبع دیگری برداشت کرده بودم. البته برای کار خودم مقداری تغییر درش داده بودم و مثلا چون magic_quotes_gpc رو خودم در جای دیگری برای تمام داده های GPC (به معنای GET, POST و COOKIE) خنثی میکنم اون خط if(get_magic_quotes_gpc()) رو درش حذف کرده بودم.

خلاصه حواستون به داده های عددی که داخل کوتیشن قرار نمیگیرن باشه. mysql_real_escape_string وقتی اثر میکنه که داده ها در کوئری در کوتیشن محصور بشن.

البته شما میتونید داده های عددی رو بصورت جداگانه ولیدیت کنید. یعنی قبل از درج در کوئری چک کنید که دقیقا فرمت یک عدد رو داشته باشن و هیچ چیز دیگری درشون نباشه. همونطور که در این تابع quote_smart مشاهده میکنید میشه با تابع is_numeric این مسئله رو براحتی چک کرد. البته این تابع اعداد اعشاری رو هم قبول میکنه، ولی بهرحال یک عدد اعشاری هم خطر SQL Injection نداره.

ضمنا فراموش نکنید SQL Injection فقط بخشی و یک نوع از ملاحظات امنیتی هر برنامه ای است. خیلی چیزها هست که به منطق و جزییات خود برنامه برمیگرده و برنامه نویس خودش شخصا میتونه تشخیص بده و هندل کنه. مثلا یه وقتی هست باید چک کنید که ورودی وارد شده علاوه بر اینکه یک عدد هست باید در محدودهء خاصی باشه یا از بین چند عدد خاص باشه یا کاربر جاری اجازهء دسترسی رو داشته باشه و غیره. اینا کار خودش شما هست و هیچ تابعی علم غیب نداره و این کارها رو نمیتونه بصورت خودکار برای شما انجام بده.

———————————

در صفحهء نمایش اکانتهای سیستم رجیستر و لاگین خودم امکان مرتب کردن بر اساس ستون رو گذاشتم. اسم هر ستون بصورت یک لینک است که وقتی کاربر روش کلیک میکنه اسم ستونی که نتایج باید بر اساس اون مرتب بشن در Query string پاس میشه و همون اسم هست که در کوئری بعد از order by درج میشه.
مثل این:

admin-accounts.php?per_page=10&page=1&sort_by=username&sort_dir=asc

که نهایتا وارد کوئری میشه:

$sort_by=$_GET['sort_by'];
$query="select * from `accounts` ... order by `$sort_by` ...";

خب ما طبیعتا نباید این پارامتر رو که از سمت کلاینت دریافت میشه بدون ولیدیت/پاکسازی/Escape کردن مستقیما در کوئری درج کنیم، وگرنه میتونه منجر به SQL Injection بشه.
اما نکته اینه که mysql_real_escape_string درمورد identifier ها قابل استفاده نیست. منظور از identifier همون اسم ستون و اینطور چیزهاست (یعنی داده نیست و اسم متغییر و اسم ستون و این حرفهاست).
یعنی این کد به ما امنیت نمیده:

$sort_by=$_GET['sort_by'];
$sort_by=mysql_real_escape_string($sort_by);
$query="select * from `accounts` ... order by `$sort_by` ...";

مثلا ما اسم ستونها رو بین backquote/Backtick میذاریم، و mysql_real_escape_string هم که با Backtick اصلا کاری نداره و اون رو Escape نمیکنه.
پس باید چکار کنیم؟
بنده قبلا در یک منبع امنیتی معتبر یادم هست در مورد استفاده از داده های غیرقابل اعتماد در identifier چیزهایی خونده بودم و توی اون منبع گفته بود که ایمن کردن این مورد خیلی ظریف و مشکله و بهتره کلا طرفش نرید!! حالا من نمیدونم دقیقا دلیلش چیه، چون توضیح زیادی نداده بود، ولی در پروژه خودم سعی کردم به این توصیه عمل کنم؛ بخاطر همین از یک روش امن تری برای ولیدیت کردن اینطور پارامترها استفاده کردم.
اینم اون روشی که بکار بردم:

if(isset($_GET['sort_by']) and
in_array($_GET['sort_by'], array('uid', 'auto', 'username', 'email', 'gender', 'banned')))
$sort_by=$_GET['sort_by'];
else $sort_by='auto';

در این روش همونطور که ملاحظه میفرمایید، بجای اینکه سعی کنیم کاراکترهای خطرناک داده های ورودی رو Escape یا حذف کنیم، چک میکنیم که دادهء ورودی برابر یکی از اعضای یک گروه محدود از عبارات (در اینجا اسم ستونها) باشه. درواقع این یک روش white list است که امنیت بالایی داره، بخصوص که هیچ کاراکتر و عبارت خاص و خطرناکی بین اعضای لیست ما وجود نداره.

اگر شما هم اطلاعات، تجربه، یا ایده ای در این مورد دارید بگید.

راستی جالب نیست که MySQL تابعی برای امن کردن identifier ها نداره؟
شاید فکر کنید این کار به سادگی اینه که چیزهایی مثل backquote رو در داده های ورودی حذف کنیم، و خودمون میتونیم تابع و روشی برای این کار بسازیم، اما من فکر میکنم ریسک این کارها خیلی زیاد باشه، چون اون منبع معتبری که خوندم (فکر میکنم owasp.org بود) به صراحت خلاف این رو گفته بود و در این مورد واقعا هشدار داده بود؛ بنابراین من عاقلانه دیدم که شخصا امن ترین روشی رو که بلدم و به ذهنم میرسه استفاده کنم. دیگه از اون روشی که گذاشتم امن تر سراغ ندارم. یعنی شما چک میکنید که داده های ورودی یکی از اعضای یک white list باشن و نه چیزی غیر از اون. لیست سفید که خودش یکی از امن ترین روشهاست، و از طرف دیگر چون در اعضای این لیست سفید بخصوص هیچ کاراکتر و عبارت خطرناک و مشکوکی نداریم دیگه میشه گفت که 99% خیالمون میتونه راحت باشه که نکتهء دیگری در کار نیست.

2 دیدگاه در “چند نکته مهم درمورد SQL Injection و mysql_real_escape_string

    • بله اینم میشه ولی این سینتاکس استاندارد نیست و توی بعضی DBMS ها باعث ایجاد خطا میشه.

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

*

شما می‌توانید از این دستورات HTML استفاده کنید: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>