تفکرات اشتباه برنامه نویسان PHP

30 بهمن 1397
PHP-common-mistakes

با سلام، مثل هر شغل دیگری در این دنیا، ما برنامه نویسان هم بعضا دچار توهماتی می شویم اما از آن مطلع نیستیم. بسیار از اوقات تصور می کنیم به دلیل اینکه فلان بحث مشهور است، حتما درست نیز می باشد اما واقعیت خلاف تصورات ماست. در این جلسه به چند مورد از اشتباهاتی که برنامه نویسان PHP در برنامه نویسی دارند میپردازیم.

البته این اشتباهات، اشتباهات افرادی نیستند که به تازگی زبان PHP را یاد گرفته اند بلکه اشتباهاتی هستند که از افراد خبره هم سر می زند.

Mysql(i)_real_escape_string از ما در برابر تزریق SQL محافظت می‌کند

ما در دوره ی آموزشی 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 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 (قسمت اول) مراجعه کنید. در این مقاله و قسمت بعدی آن این مبحث را کاملا توضیح داده ایم.

escape کردن داده های کاربر برای جلوگیری از SQL Injection

یک اشتباه بسیار عجیب! حتی OWASP (پروژه امنیت نرم‌افزاری تحت وب) نیز آن را در لیست توصیه های خود قرار داده است! دو قسمت اصلی این حرف یعنی "escape کردن" و "داده های کاربر" یا (user input) مشکل دار هستند!

  • escape کردن یا فراری دادن: بالاتر هم گفتیم که escape کردن ربطی به امنیت و حملات تزریق SQL ندارد. برای این کار حتما از prepared statement ها استفاده کنید، نه escape کردن داده ها!
  • user input یا داده های کاربر: وقتی از این عبارت گُنگ استفاده می شود، در ذهن مخاطب اینطور جا می افتد که باید تنها داده های کاربر را چک کرد یا روی آن عملیاتی انجام داد اما بالاتر گفتیم که اگر چنین کاری کنید، احتمال حملات دسته دوم SQL Injection را بالا می برید. به عقیده ی من نباید از عبارت "user input" در مباحث SQL Injection استفاده شود چرا که:
    • لایه ی پایگاه داده در یک اپلیکیشن بزرگ و چند لایه نمی تواند منبع داده ها را تشخیص دهد و بگوید فلان داده امن است اما فلان داده ی دیگر امن نیست. بنابراین باید با تمام داده ها به یک شکل برخورد کرد.
    • محافظت در برابر SQL Injection، در برابر syntax error ها (خطاهای مربوط به نحوه ی نوشتار کد ها و دستورات) نیز محافظت ایجاد می کند. بنابراین باز هم دلیلی برای رها کردن prepared statement ها نداریم.
    • بالاتر هم دیدیدم که در حملات SQL Injection دسته دوم داده از خود برنامه ی شما می آید، اما باز هم باعث ایجاد مشکل و SQL Injection می شود.
    • "داده های کاربر" می تواند برداشت های مختلفی برای برنامه نویسان ایجاد کند؛ به طور مثال ممکن است فردی فکر کند هر داده ای که در سرور ننویسیم، داده ی کاربر محسوب می شود. فرد دیگر تصور کند اگر داده ای از سمت فرم ها بیاید داده ی کاربر محسوب می شود. اگر در یک فرم داده ای داشته باشیم که خودمان نوشته باشیم (مانند منو های select)، می توان گفت این داده یک داده ی کاربر است یا خیر؟ و هزاران سوال دیگر.

نکته: ما نمی گوییم escape کردن داده ها را کلا از ذهنتان بیرون کنید، میگوییم escape کردن داده ها برای ایمن سازی در برابر SQL Injection نیست. روش بسیار بهتری به اسم prepared statement وجود دارد و escape کردن کار عاقلانه ای نیست.

Mysqli برای افراد مبتدی است، PDO برای افراد حرفه ای

این حرف اشتباهی است که متاسفانه بین برنامه نویسان باب شده است. دو نکته در رابطه با این بحث وجود دارد:

اولا: PDO امن تر و آسان تر از Mysqli است. بله آسان تر! بنابراین مبتدی و غیر مبتدی ندارد و همه باید از گزینه ی بروز تر و بهتر استفاده کنند.

دوما: اگر بخواهیم چنین حرفی بزنیم، باید دقیقا برعکس آن را بگوییم! اگر تمام بدبختی های استفاده از prepared statement ها در mysqli را در نظر بگیریم (مثلا اگر یادتان باشد، دریافت یک آرایه ی متناظر یا تعداد ردیف ها از یک prepared statement ممکن نیست مگر اینکه از حقه هایی استفاده کنیم) و از طرف دیگر تمام راحتی های استفاده از PDO را نیز در نظر بگیریم (مثل API بسیار راحت و ساده)، باید بگوییم تنها افرادی مجاز به استفاده از mysqli هستند که حرفه ای باشند و کاملا بدانند از هر چیزی کجا استفاده کنند و تجربه ای طولانی در برنامه نویسی داشته باشند چرا که خرابکاری در mysqli بسیار راحت تر از PDO است.

استفاده ی بیش از حد از number of rows با کوئری SELECT

هر زمان که خواستید چنین کاری را انجام دهید، بدانید که کار اشتباهی می کنید! در مواقعی که به آن نیاز داریم اصلا در دسترس نیست! موارد استفاده ی اشتباه از آن معمولا شامل موارد زیر می شود.

معمولا افراد تازه کار از SELECT برای دریافت تعداد ردیف ها استفاده می کنند! این کار بسیار اشتباهی است چرا که با این کار پایگاه داده ی خود را مجبور می کنید تا تمام ردیف ها را انتخاب کند، سپس همه ی آن ردیف ها را به علاوه ی تعدادشان به PHP بفرستد! روش صحیح این است که از پایگاه داده بخواهید تنها تعداد ردیف ها را برگرداند. این کار با دستور (*)SELECT count عملی است.

برخی اوقات از این دستور برای این استفاده می شود که ببینند آیا کوئریِ ما داده ای برگردانده است یا خیر. این هم کار عجیبی و غریبی است چرا که همیشه خود داده را داریم:

  • اگر تنها یک ردیف را می خواهید، آن را fetch کرده و داخل یک شرط قرار دهید. اگر داده ای وجود داشته باشد، شرط برقرار است چرا که مساوی با true است.
  • اگر چندین ردیف را می خواهید، تمام شان را در یک آرایه fetch کنید و باز هم مانند مورد قبلی، آن ها را در یک شرط قرار دهید. اگر تعداد ردیف ها را نیز بخواهید می توانید از ()count روی این آرایه استفاده کنید.

غیر فعال کردن گزارش خطا ها به دلایل امنیتی

شما می توانید پیغام های خطا را به دو شکل غیر فعال کنید:

  • به صورت محلی و با استفاده از @
  • به صورت گلوبال و با استفاده از (0)error_reporting

کاربران معمولا گزارش خطا را غیر فعال می کنند تا پیام خطا روی صفحه نمایش داده نشود و اطلاعات حساس در دست عموم مردم قرار نگیرد. در هر دو صورت، اگر کد شما تازه نوشته شده است، باید گزارش خطا را برای کد خود فعال کنید و از @ نیز استفاده نکنید تا در صورت بروز مشکل، بدانید اشکال از کجاست و آن را برطرف کنید. شاید بپرسید نمایش اطلاعات حساس برای کاربران چه می شود؟ برای جلوگیری از نمایش خطا ها برای کاربران از دستور زیر استفاده کنید:

ini_set('display_errors', 0);

بدین صورت PHP خطا ها را log می کند و به کسی جز شما نمایش نمی دهد.

عبارت ((If (isset($var) && !empty($var

به کد زیر توجه کنید:

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 است!

باورتان می شود؟ این همه زیاده نویسی برای یک کلمه دستور؟!

استفاده از ساختار try ... catch برای نمایش خطا

کار بسیار عجیبی است که بیاییم یک exception را catch کنیم تا فقط نمایش دهیم! چرا؟ به دلیل اینکه exception هایی که catch نشده باشند (یعنی حالت uncaught exception) به خودی خود یک fatal error است، بنابراین خود به خود نمایش داده می شود و نیازی به ردیف کردن دستورات try/catch/die نیست. با این کار فقط کد خود را شلوغ تر می کنید.

متاسفانه برنامه نویسان از توضیح سایت رسمی PHP برداشت اشتباه کرده اند. سایت رسمی PHP از این مثال ها در دو جهت استفاده کرده است:

  • برای یادگیری و نه برای ساخت وب سایت واقعی!
  • حتی اگر گزارش خطا (error reporting) توسط کاربر غیر فعال شده باشد، باز هم پیام های خطای try ... catch نمایش داده می شوند؛ بنابراین زمانی به درد می خورد که خطا ها را به صورت سراسری (global) غیر فعال کرده باشیم اما بخواهیم در یک یا چند مورد خاص، خطا ها را مشاهده کنیم.

استفاده از HTTP_X_FORWARDED_FOR و امثال آن برای دریافت آدرس IP "واقعی"

بعضی از برنامه نویسان برای دریافت آدرس 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).

علامت های single quotes از double quotes سریع تر هستند

می شود ساعت ها در این مورد بحث کرد اما خلاصه برایتان می گویم که سرتان به درد نیاید: چنین چیزی از بیخ و بُن ساخته و پرداخته ی ذهن افرادی است که چیزی از برنامه نویسی نمی دانند.

اگر خیلی به این بحث علاقه مند هستید توضیح خلاصه ای می دهم:

ما چیزی به نام opcode cache داریم که کد های parse شده ی PHP را در کَش ذخیره می کند. چه از علامت single quotes یعنی ' ' و چه از double quotes یعنی " " استفاده کنید، هر دو به صورت opcode مشابه ذخیره می شوند، بنابراین حتی در تئوری نیز نمی توانیم تفاوتی برایشان قائل شویم چه رسد به حالت عملی. توجه کنید که ریزه کاری تا این حد هیچ گاه موجب پیشرفت و بهتر شدن کد شما نمی شود. بهینه سازی همیشه در مراحل اصلی و بزرگ و یا در مراحل دستکاری داده ها اتفاق می افتند و تا به حال هیچ برنامه ای در دنیا با تغییر ' ' به  " " بهبود نیافته است! هر چیزی را در اینترنت باور نکنید.

سعی کنید همیشه از اشتباهات برنامه نویسان دیگر و داستان هایی که برایتان تعریف می کنند (البته اگر با دلیل و منطق باشد) درس عبرت بگیرید. به هر حال امیدوارم از این مقاله لذت برده باشید.

نویسنده شوید
دیدگاه‌های شما (3 دیدگاه)

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

star
13 اردیبهشت 1400
سلام واقعا مقالتون در مورد اشتباهات رایج برنامه نویسان php عالی بود ممنون!

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

ااا
12 فروردین 1398
مورد آخر برخی مواقع صحیح است. single quotes ها از نظر کاراکتر $ نادیده گرفته می‌شوند. پس تجزیه‌ی قدری سریع‌تر و امن‌تر دارند.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

امیر زوارمی
12 فروردین 1398
با سلام دوست عزیز، مقالات بسیار زیادی در این مورد نوشته شده اند و از حدود سال 2012 این بحث کنار گذاشته شده است. Single quote ها نه در هنگام runtime و نه در هنگام compile time سریعتر عمل نمی کنند. چیزی که بهش اشاره میشه تست هایی مثل این مورد هست: Single quotes: 0.061846971511841 seconds Double quotes: 0.061599016189575 seconds توجه کنید که این اعداد حتی در سطح عظیم تجاری هم هیچ تفاوتی ایجاد نمی کنند چه برسه به یک وب سایت که شاید روزانه بیشتر از 100 نفر هم کاربر نداره مقاله ی جامعی که این مورد رو بررسی کرده: https://nikic.github.io/2012/01/09/Disproving-the-Single-Quotes-Performance-Myth.html

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

محمد حسین زارعی
22 اسفند 1397
سلام. اینا چطوری اشتباهات رایج هستن؟ به ذهن منم خطور نمیکرد اینا!

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

امیر زوارمی
12 فروردین 1398
با سلام دوست عزیز، این ها اشتباهات رایجی هستن که برنامه نویسان PHP به اونها دچار میشن. وقتی میگیم برنامه نویسان PHP دیگه منظورمون افراد مبتدی یا افراد در حال یادگیری نیست؛ منظورمون واقعا برنامه نویسان PHP هست. برای همین اینها برای افرادی که تازه PHP رو یاد میگیرن عجیبه و به قول شما واقعا اشتباه رایج نیست.

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.