با سلام، مثل هر شغل دیگری در این دنیا، ما برنامه نویسان هم بعضا دچار توهماتی می شویم اما از آن مطلع نیستیم. بسیار از اوقات تصور می کنیم به دلیل اینکه فلان بحث مشهور است، حتما درست نیز می باشد اما واقعیت خلاف تصورات ماست. در این جلسه به چند مورد از اشتباهاتی که برنامه نویسان PHP در برنامه نویسی دارند میپردازیم.
البته این اشتباهات، اشتباهات افرادی نیستند که به تازگی زبان PHP را یاد گرفته اند بلکه اشتباهاتی هستند که از افراد خبره هم سر می زند.
ما در دوره ی آموزشی PDO در مورد مبحث SQL Injection (تزریق SQL) صحبت کرده ایم. این فکر اشتباه که تابع Mysql(i)_real_escape_string
از ما در برابر تزریق SQL محافظت می کند به دلیل برداشتی اشتباه از وب سایت رسمی PHP به وجود آمده که می گوید:
"This function must always (with few exceptions) be used to make data safe".
در واقعیت این تابع هیچ ارتباطی با محافظت از SQL Injection ندارد و کار اصلی آن این است که در رشته های literal از SQL، کاراکتر های ویژه (مثل " و ; و ...) را escape کند. حال در نتیجه ی این کار آن ها در برابر SQL Injection محافظت می شوند اما در هر نوع کوئری دیگر (چه نام جدول ها و چه literal های عددی) به هیچ دردی نمی خورند و از هیچ چیزی محافظت نمی کنند.
نکته: مقادیر literal مقادیری هستند که مستقیما تعیین می شوند نه از طریق محاسبات و توابع دیگر. به طور مثال، عدد 1 در عبارت x = 1
از نوع literal است (مستقیما وارد سورس کد شده است) اما با اینکه عبارت (x = cos(0
(یعنی کوسینوسِ صفر) برابر با 1 است، دیگر این 1 از نوع literal نیست، چرا که مستقیما در سورس کد تعریف نشده بلکه طی یک عملیات محاسباتی یا توسط توابع دیگر به عنوان خروجی برگردانده شده است (به عبارت دیگر، در سورس کد وارد نشده، بلکه به وجود آمده). بر این اساس وقتی می گوییم literal رشته ای (string literals) منظورمان مقادیری مثل 'a string' است.
ما قبلا در رابطه با تزریق SQL صحبت کرده ایم. به مقالات زیر مراجعه کنید:
اجرای کوئری ها و SQL Injection (قسمت اول)
اجرای کوئری ها و SQL Injection (قسمت دوم)
تزریق SQL دسته دوم (Second Order SQL Injection) زمانی رخ می دهد که شما داده ای را از سمت کاربر دریافت می کنید و کاراکتر های خاص آن را escape کرده و آن را در پایگاه داده ذخیره می کنید. حالا وب سایت شما بعدا می خواهد دوباره از آن داده استفاده کند و زمانی که چنین کاری انجام می دهد SQL Injection رخ میدهد. متاسفانه بین برنامه نویسان PHP شایع است که prepared statement ها تنها از حملات SQL Injection دسته اول محافظت می کنند نه دسته دوم. این حرف کاملا اشتباه است.
SQL Injection دسته دوم تنها زمانی اتفاق می افتد که شما یکی از prepared statement ها را نادیده بگیرید! یعنی چه؟ یعنی برخی از برنامه نویسان اشتباه بزرگی می کنند و با خود می گویند فلان داده امن نیست بنابراین برایش از prepared statement ها استفاده می کنم اما فلان داده ی دیگر امن است بنابراین نیازی به استفاده از prepared statement ها نیست.
زمانی که چنین اشتباه بزرگی را مرتکب شوید انواع SQL Injection ممکن می شود اما اگر در همه جا از prepared statement ها استفاده کنید، هیچ نوع حمله ی دست اول، دست دوم یا دست نود و نهم! ممکن نخواهد بود.
اگر نمی دانید prepared statement ها چه هستند به مقاله ی اجرای کوئری ها و SQL Injection (قسمت اول) مراجعه کنید. در این مقاله و قسمت بعدی آن این مبحث را کاملا توضیح داده ایم.
یک اشتباه بسیار عجیب! حتی OWASP (پروژه امنیت نرمافزاری تحت وب) نیز آن را در لیست توصیه های خود قرار داده است! دو قسمت اصلی این حرف یعنی "escape کردن" و "داده های کاربر" یا (user input) مشکل دار هستند!
نکته: ما نمی گوییم escape کردن داده ها را کلا از ذهنتان بیرون کنید، میگوییم escape کردن داده ها برای ایمن سازی در برابر SQL Injection نیست. روش بسیار بهتری به اسم prepared statement وجود دارد و escape کردن کار عاقلانه ای نیست.
این حرف اشتباهی است که متاسفانه بین برنامه نویسان باب شده است. دو نکته در رابطه با این بحث وجود دارد:
اولا: PDO امن تر و آسان تر از Mysqli است. بله آسان تر! بنابراین مبتدی و غیر مبتدی ندارد و همه باید از گزینه ی بروز تر و بهتر استفاده کنند.
دوما: اگر بخواهیم چنین حرفی بزنیم، باید دقیقا برعکس آن را بگوییم! اگر تمام بدبختی های استفاده از prepared statement ها در mysqli را در نظر بگیریم (مثلا اگر یادتان باشد، دریافت یک آرایه ی متناظر یا تعداد ردیف ها از یک prepared statement ممکن نیست مگر اینکه از حقه هایی استفاده کنیم) و از طرف دیگر تمام راحتی های استفاده از PDO را نیز در نظر بگیریم (مثل API بسیار راحت و ساده)، باید بگوییم تنها افرادی مجاز به استفاده از mysqli هستند که حرفه ای باشند و کاملا بدانند از هر چیزی کجا استفاده کنند و تجربه ای طولانی در برنامه نویسی داشته باشند چرا که خرابکاری در mysqli بسیار راحت تر از PDO است.
هر زمان که خواستید چنین کاری را انجام دهید، بدانید که کار اشتباهی می کنید! در مواقعی که به آن نیاز داریم اصلا در دسترس نیست! موارد استفاده ی اشتباه از آن معمولا شامل موارد زیر می شود.
معمولا افراد تازه کار از SELECT برای دریافت تعداد ردیف ها استفاده می کنند! این کار بسیار اشتباهی است چرا که با این کار پایگاه داده ی خود را مجبور می کنید تا تمام ردیف ها را انتخاب کند، سپس همه ی آن ردیف ها را به علاوه ی تعدادشان به PHP بفرستد! روش صحیح این است که از پایگاه داده بخواهید تنها تعداد ردیف ها را برگرداند. این کار با دستور (*)SELECT count
عملی است.
برخی اوقات از این دستور برای این استفاده می شود که ببینند آیا کوئریِ ما داده ای برگردانده است یا خیر. این هم کار عجیبی و غریبی است چرا که همیشه خود داده را داریم:
()count
روی این آرایه استفاده کنید.شما می توانید پیغام های خطا را به دو شکل غیر فعال کنید:
@
(0)error_reporting
کاربران معمولا گزارش خطا را غیر فعال می کنند تا پیام خطا روی صفحه نمایش داده نشود و اطلاعات حساس در دست عموم مردم قرار نگیرد. در هر دو صورت، اگر کد شما تازه نوشته شده است، باید گزارش خطا را برای کد خود فعال کنید و از @
نیز استفاده نکنید تا در صورت بروز مشکل، بدانید اشکال از کجاست و آن را برطرف کنید. شاید بپرسید نمایش اطلاعات حساس برای کاربران چه می شود؟ برای جلوگیری از نمایش خطا ها برای کاربران از دستور زیر استفاده کنید:
ini_set('display_errors', 0);
بدین صورت PHP خطا ها را log می کند و به کسی جز شما نمایش نمی دهد.
به کد زیر توجه کنید:
if (isset($someVar) && !empty($someVar))
این کد بسیار زیاد استفاده می شود و حتما آن را در کد های دیگران نیز دیده اید. کاربران اکثرا فکر می کنند دستور (if(empty($someVar
مشابه دستور (if($someVar
است؛ به عبارت دیگر تصور می کنند ()empty
مقادیری را که empty هستند اما در ظاهر نشان نمی دهند (مانند رشته های خالی، عدد صفر و ...) چک می کند اما شما می توانید برای این کار به سادگی از خود متغیر استفاده کنید. به دلیل قابلیت type juggling در PHP، اگر متغیری را در شرطی بگذارید، عبارت شرطی آن متغیر را تبدیل به داده ی بولین می کند و به این ترتیب خود به خود خالی بودن (empty) متغیر را نیز چک می کند. بر اساس چیزی که گفتیم کد بالا می تواند به کد زیر تغییر کند:
if (isset($someVar) && $someVar)
اما نکته ی خنده دار آن جاست که کد بالا دقیقا تعریف تابع ()empty!
است! می توانید صحت این موضوع را از صفحه ی رسمی وب سایت PHP چک کنید. بنابراین می توان گفت استفاده از ()isset
و ()empty
در یک شرط، محکم کاری بیش از حد است؛ البته به کد شما آسیبی وارد نمی کند اما نیازی به این کار نیست. بر اساس چیزی که گفته شد می توانیم کد بالا را به صورت زیر خلاصه کنیم:
if (!empty($someVar))
باورتان نمی شود اگر بگویم مسئله هنوز هم تمام نشده است! در بعضی از موارد دیده شده است که برنامه نویسان از عمد از دستور ()empty
برای متغیری استفاده می کنند که می دانند وجود دارد. این کار یک زیاده نویسی دیگر است چرا که در چنین حالتی دستور (if(!empty($var)
دقیقا مشابه (if($var
است!
باورتان می شود؟ این همه زیاده نویسی برای یک کلمه دستور؟!
کار بسیار عجیبی است که بیاییم یک exception را catch کنیم تا فقط نمایش دهیم! چرا؟ به دلیل اینکه exception هایی که catch نشده باشند (یعنی حالت uncaught exception) به خودی خود یک fatal error است، بنابراین خود به خود نمایش داده می شود و نیازی به ردیف کردن دستورات try/catch/die نیست. با این کار فقط کد خود را شلوغ تر می کنید.
متاسفانه برنامه نویسان از توضیح سایت رسمی PHP برداشت اشتباه کرده اند. سایت رسمی PHP از این مثال ها در دو جهت استفاده کرده است:
بعضی از برنامه نویسان برای دریافت آدرس IP واقعی (توضیح می دهم چرا واقعی نیست) از دستوراتی مثل HTTP_X_FORWARDED_FOR یا HTTP_X_CLIENT_IP و غیره استفاده می کنند و تصور می کنند کارشان از REMOTE_ADDR
بسیار بهتر است. اگر حوصله ی خواندن کامل مطلب را ندارید ابتدا خلاصه اش را برایتان می گویم:
در بحث امنیت به هیچ چیز به جز REMOTE_ADDR
اعتماد نکنید!
آقای آنتونی فِرارا در سال 2012 خاطره ای در این باره منتشر کرد که مورد توجه بسیار زیادی قرار گرفت و من هم پیشنهاد می کنم اگر زبان انگلیسی بلد هستید این خاطره را مطالعه کنید (مطالعه ی کامل داستان).
در واقع هر چیزی به جز REMOTE_ADDR
تنها یک هدر HTTP است (HTTP header) بنابراین هر کسی می تواند آن را spoof کند.
اگر با حملات spoofng آشنایی ندارد به صورت خلاصه می توان گفت:
در زمینه امنیت شبکه، یک حمله کلاهبرداری یا spoofing، یک موقعیت است که در آن یک شخص یا برنامه به عنوان یکی دیگر خود را معرفی کرده ویا با جعل دادهها موفق به دست آوردن مزیت نامشروع میشود. این نوع از حمله به روشهای مختلفی رخ میدهد. به عنوان مثال، هکر میتواند از آدرس IP جعلی برای رسیدن به اهداف خود کمک بگیرد. همچنین، یک مهاجم ممکن است ایمیلهای جعلی ارسال کند یا وب سایتهای جعلی به منظور جذب کاربران و بدست آوردن: نام ورود به سیستم، کلمه عبور، اطلاعات حساب کاربران و.. راه اندازی کند.
البته یک استثناء وجود دارد: اگر اسکریپت PHP شما پشت پراکسی خاص و مطمئنی قرار دارد و مطمئن هستید کدام HTTP header/env ای که پراکسی تعیین می کند شامل IP خارجی است، می توانید از آن متغیر استفاده کنید. بهتر از آن این است که پراکسی را طوری تنظیم کنید که remote IP را مستقیما داخل REMOTE_ADDR تزریق کند (مثل استفاده از دستور ;fastcgi_param REMOTE_ADDR $http_x_real_ip
برای nginx).
می شود ساعت ها در این مورد بحث کرد اما خلاصه برایتان می گویم که سرتان به درد نیاید: چنین چیزی از بیخ و بُن ساخته و پرداخته ی ذهن افرادی است که چیزی از برنامه نویسی نمی دانند.
اگر خیلی به این بحث علاقه مند هستید توضیح خلاصه ای می دهم:
ما چیزی به نام opcode cache داریم که کد های parse شده ی PHP را در کَش ذخیره می کند. چه از علامت single quotes یعنی ' ' و چه از double quotes یعنی " " استفاده کنید، هر دو به صورت opcode مشابه ذخیره می شوند، بنابراین حتی در تئوری نیز نمی توانیم تفاوتی برایشان قائل شویم چه رسد به حالت عملی. توجه کنید که ریزه کاری تا این حد هیچ گاه موجب پیشرفت و بهتر شدن کد شما نمی شود. بهینه سازی همیشه در مراحل اصلی و بزرگ و یا در مراحل دستکاری داده ها اتفاق می افتند و تا به حال هیچ برنامه ای در دنیا با تغییر ' ' به " " بهبود نیافته است! هر چیزی را در اینترنت باور نکنید.
سعی کنید همیشه از اشتباهات برنامه نویسان دیگر و داستان هایی که برایتان تعریف می کنند (البته اگر با دلیل و منطق باشد) درس عبرت بگیرید. به هر حال امیدوارم از این مقاله لذت برده باشید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.