به طور معمول در طراحی یک نرمافزار تحت وب و به خصوص وقتی از فریمورک ها استفاده میکنیم، خیلی درگیر مسائل جزئی شبکه و امنیتی نمیشویم. اکثرا خود فریمورک یا لایبریها به طور تقریبا جادویی و اتوماتیک، با گذاشتن چند تگ یا ارثبری از یک کلاس توسط ما، تمام این موارد رو رعایت میکنند.
اما بد نیست یک بار هم که شده نگاه جزئیتر به چگونگی کارکرد این پروتوکل و فیچر های مختلف آن که در واقع بستر تمام نقل و انتقالات اپ تحت وب ما هستند بیاندازیم و فلسفهی وجودیشان را بررسی کنیم. در این مقاله قصد دارم برخی از هدر ها و مکانیزمهای پروتکل http که از لحاظ امنیتی اهمیت ویژهای دارند و ممکن است ما به عنوان یک توسعهدهنده وب با آنها بیشتر سر و کار داشته باشیم را بررسی کنیم.
حتما با متودها (افعال) HTTP آشنا هستید. انواع ریکوئست هایی شامل GET, POST و غیره. این متودها علاوه بر تاثیر معنایی که دارند (یعنی شفاف کردن هدف یک ریکوئست و API هایمان)، از لحاظ امنیتی هم مهم هستند و باید در استفاده از آنها دقت کرد.
طبیعتا GET ریکوئست همانطور که از نامش پیداست برای دریافت و خواندن اطلاعات از سرور هست و POST برای ارسال و یا تغییر اطلاعات. حال فرض کنید برای فرستادن اطلاعات به جای POST از GET استفاده کنیم، چه اشکالی ممکن است رخ دهد؟ بالاخره خود GET ریوکوئست امکان فرستادن پارامتر را دارد؛ چرا از همانها استفاده نکنیم؟
فرض کنید برای لاگین، به نوعی API را طراحی کردیم که اطلاعات ورود (یوزر و پسورد) را به عنوان پارامتر باید ارسال کنیم. کاربر اطلاعات را وارد میکند و بدون مشکل به سرور میروند. اما نکتهی امنیتی که وجود دارد این است که این URL مثل تمام آدرس های دیگری که کاربر به آنها سر زده در هیستوری مرورگر ذخیره شده. همانطور هم که گفتیم رمز کاربر به عنوان پارامتر بخشی از این آدرس هست، پس رمز کاربر هم به صورت تکست معمولی قابل مشاهده است. همچنین کاربر ممکن است حواسش نباشد و آدرس سایت را همراه با این پارامتر ها به اشتراک بگذارد. یا بدتر اگر سایت از HTTPS استفاده نکند خود URL و تمام پارامترها در معرض دید همه از جمله ISP، مودم، و غیره قرار میگیرد! (البته اگر سایتتان از HTTPS استفاده نمیکند مشکلاتتان فقط محدود به این مورد نیست.) برای همین این اطلاعات مخفی باید در یک POST ریکوئست فرستاده شود چرا که بدنه یک ریکوئست از نوع POST مخفی، غیر قابل انتشار و رمزنگاری شده است.
خب حالا فرض کنید اطلاعاتی که میخواهیم ارسال کنیم اصلا اهمیت مخفی بودن ندارند، مثلا صرفا آیدی یک آبجکت که میخواهیم از دیتابیس پاک کنیم را بفرستیم. یک عدد ساده. پس بیایم از GET استفاده کنیم.
حالا من به عنوان یک فرد مخرب همین لینک را جای دیگری مثلا در یک ایمیل یا سایت دیگر قرار میدهم. اصلا با یک سرویس کوتاهکننده لینک آن را تغییر میدهم. شما هم از همه جا بیخبر روی لینک (تله) من کلیک میکنید و درخواست با کوکیها و اطلاعات آثنتیکیشن شما به سرور ارسال میشود و پروفایلتان پاک میشود! یا فلان حساب بانکیتان خالی میشود یا ... .
خب چرا استفاده از GET باعث این اتفاق شد و چگونه POST از این مشکل جلوگیری میکند؟ چون POST حتما باید از طریق یک فرم HTML ارسال بشود. یعنی حتما مرورگر باید یک فرم را لود کند و درخواست از آنجا ارسال شود. اینطوری با گذاشتن یک لینک در جاهای متفرقه اتفاقی نمیافتد و در آن صورت کاربر ارور 405: Method Not Allowed را دریافت میکند. از طرفی با مکانیزم های Same Origin و CSRF که در ادامه توضیح میدهم و از قابلیت های POST هستند، از جعل یک فرم یا فریب دادن کاربر جلوگیری میکنند و در نهایت مطمئن میشویم که کاربر قصد ارسال همین درخواست را داشته.
البته که استفاده از فعل های دیگر مثل PUT, DELETE, UPDATE هم از لحاظ معماری API مهم است و اگر بخواهیم یک API واقعا RESTful داشته باشیم باید از تمام قواعد آن استفاده کنیم، اما آنچه از لحاظ امنیتی خیلی مهم است اشتباه نگرفتن POST و GET در درخواستهایمان است.
این فیچر بسیار مهم اما بسیار منفور در بین برنامهنویسان میباشد، قطعا در توسعه یک اپ (مخصوصا اگر فرانت کار کردید) با ارور CORS برخورد کردید. بیاید علت وجود این مکانیزم را بررسی کنیم.
در حالت کلی مرورگر شما تنها اجازه میدهد به دامنهای که روی آن هستید درخواست ارسال کنید. به طور مثال فقط وقتی میتوانید به سرور ویرگول درخواست بفرستید که در سایت virgool.ir باشید. اگر این محدودیت نبود ممکن بود یک سایت مخرب بدون اطلاع شما از طریق مرورگرتان به سرور بانک ملت درخواست بفرستد و اطلاعات حساب بانکیتان را از این طریق دریافت کند. اما حالا خود مرورگر وقتی متوجه میشود که سایتی میخواهد به سروری دیگر درخواست بفرستد، جلوی آن درخواست را میگیرد.
هدر origin که خود به خود توسط مرورگر گذاشته میشود نشان میدهد که درخواست از کدام دامنه آمده. به زبان سادهتر کاربر در چه صفحهای بوده که این درخواست را ارسال کرده.
سرور با چک این هدر مطمئن میشود که درخواست از جایی آمده که انتظارش میرود. هدر origin را خود مرورگر ها قرار میدهند و قابل تغییر توسط سایت نیست. لذا به اصطلاح جزو هدر های ممنوعه یا forbidden است. دقت شود که این مکانیزم به منظور محافظت از کاربر است تا به طور ناخواسته مورد سواستفاده قرار نگیرد؛ وگرنه خود کاربر میتواند هدر را تغییر دهد. یعنی هدر origin و بلاک کردن درخواست های متفرقه برای جلوگیری از DDOS Attack یا هک CSRF نیست و تنها یک سیستم امنیتی سمت کاربر است.
اما ممکن است واقعا بخواهیم به یک سایت دیگر درخواست بفرستیم. مثلا از API یک سایت دیگر استفاده کنیم یا اصلا سرور ما روی یک دامنهی دیگری باشد. چه میتوانیم بکنیم که مرورگر این درخواستهای روا را نیز بلاک نکند. اینجا از مکانیزمی به نام CORS که مخفف Cross Origin Resource Sharing میباشد استفاده میکنیم.
در واقع وقتی که مرورگر میبیند یک سایت به سروری بر روی دامنهای دیگری درخواست فرستاده، یک درخواست از نوع OPTION به سرور مد نظر میفرستد. به این درخواست Preflighted request گفته میشود. در واقع مرورگر یک چک با سرور میکند که آیا مبدا این درخواست قانونی و مورد انتظار هست یا نه. به سرور میگوید «یک درخواست از همچین دامنهای دارم، اوکیای؟ میشناسی؟». در این مواقع سرور هدر Access-Control-Allow-Origin را جزو ریسپانش بر میگرداند. این هدر به مرورگر میگوید که منِ سرور انتظار دریافت ریکوئست از چه مبدا هایی را دارم. اگر مبدا کاربر جزو مبدا های مشخص شده توسط سرور بود، یعنی مبدا مورد اعتماد سرور هست و مرورگر درخواست اصلی را میفرستد. مقدار این هدر میتواند دامنه چند سایت باشد یا کاراکتر ستاره (*) به معنای قبول درخواست از تمام مبداها باشد.
Access-Control-Allow-Origin: https://virgool.ir, https://noghte.ir
Access-Control-Allow-Origin: *
همانطور که گفته شد تمام این داستان برای حفاظت از کاربر است و سمت مرورگر اعمال میشود. شما میتوانید از طریق افزونههای موجود CORS را غیر فعال کنید و از هر سایتی به هر سایت دیگری درخواست بفرستید. همچنین وقتی از طرق دیگر به جز مرورگر (مثلا از سروری دیگر یا با استفاده از Postman) درخواست میفرستید، دیگر نیازی نیست نگران Origin و CORS باشید.
احتمالا وقتی با فرم های HTML کار کردید، اصطلاح CSRF به گوشتان خورده است. برای استفاده از CSRF در واقع شما یک ورودی مخفی در فرمتان قرار میدهید که مقدار آن را سرور در زمانی که خود فرم را برایتان ارسال کرده، میدهد. این تگ از نوع مهمی از حمله به نام Cross-Site-Resource-Forgery جلوگیری میکند. بیایید این نوع هک را با یک مثال توضیح دهیم.
فرض کنید شما استاد درس طراحی نرمافزار هستید و در سامانه پورتال میخواهید نمرهی یکی از دانشجویانتان را ثبت کنید. خب در حالت عادی شمارهی دانشجو را با نمرهاش در یک فرم وارد میکنید و این اطلاعات در قالب یک POST Request ارسال میشوند. اما یک دانشجوی بدجنس میخواهد نمرهی خودش را ۲۰ کند! او در پروژهی طراحینرم افزارش یک فرم مخرب قرار میدهد، در ظاهر این فرم مربوط به کارکرد خود سایت است (مثلا ثبتنام یا هر چیز) شما وارد سایت این دانشجو میشوید و فرم را ارسال میکنید. اما در حقیقت زیر این فرم یک درخواست پست به پورتال دانشگاه با محتوای شماره دانشجویی این دانشجو و نمرهی ۲۰ میباشد! اگر هدر Access-Control-Allow-Origin سایت دانشگاه اشتباه ست شده باشد یا اصلا سایت پروژه دانشجو هم جزو هدر های مجاز باشند، مرورگر شما از همه جا بیخبر کوکیهای ذخیره شدهتان که حاوی اطلاعات ورود به حساب کاربری پورتالتان هست در بالای این درخواست قرار میدهد و به سرور دانشگاه ارسال میکند. نمرهی این دانشجو ۲۰ ثبت میشود در صورتی که شما اصلا خبر نداشتید همچین درخواستی ارسال کردید.
اینجاست که سرور باید مطمئن باشد که شما کاملا در جریان این درخواست POST که دارید میفرستید هستید و در واقع وقتی دکمهی Submit فرم سایت را زدید، داشتید به همین درخواست ارسال شده پاسخ میدادید. لذا سرور وقتی برای شما خود فرم خالی را میفرستد، همزمان یک تگ مخفی هم برایتان میفرستد. سرور میگوید وقتی که خواستی جواب بدی این تگ را هم ضمیمهی درخواستت بکن. سپس وقتی POST Request را میفرستید علاوه بر اطلاعات تگ مخفی را هم ارسال میکنید. این تگ فقط مخصوص شما و فقط مخصوص همین فرم بوده. وقتی سرور این تگ را میبیند خیالش راحت میشود که این درخواست پیرو همین فرم مربوطه بوده و نه فرم دیگری. CSRF را از سرور گرفتیم و به عنوان یک ورودی در فرم قرار دادیم.
همانطور که میدانید پروتکل http یک پروتکل بدون حالت (Stateless) میباشد. این یعنی درخواست های http به همدیگر ارتباط ندارند و هر درخواست به صورت جداگانه بررسی میشود.
برای همین باید در هر درخواستمان خودمان را معرفی کنیم که چه کسی بودیم و سرور با ما مثل یک آدم غریبه برخورد نکند. از همین جهت از توکن های آثنتیکیشن استفاده میکنیم. این توکن ها در واقع همان علامت مخصوص حاکم بزرگ، میتی کومان هستند که با نشان دادن آن سرور میفهمد ما کی هستیم و راهمان میدهد.
این توکن ها انواع مختلف دارند. مثلا میتوانند API key باشند که در واقع یک رشته مثل پسورد است یا از نوع JWT باشند که مثلا ویرگول از این نوع توکن استفاده میکند. (اگر باورم ندارید تو هدر درخواستتون ببینید). مثلا در نوع bearer یک هدر به نام Authorization وجود دارد:
Authorization: Bearer eyJhbGciOiJIUzI1...
که در واقع این رشته را سرور زمان لاگین به کاربر داده و در تمام درخواست های بعدی کاربر به سرور آن را میفرستد.
قطعا HTTP و کلا پروتکل های وب متشکل از اجزای زیادی هستند که هر کدام وظیفهای از لحاظ امنیتی یا کاربردی دارند و تمام نقل و انتقالات دنیای امروز رو ممکن میکنند. در این مقاله سعی کردم که چند مورد اصلی که یک برنامهنویس وب ممکن است بیشتر با آنها سر و کار داشته باشد را به طور اجمالی معرفی کنم. امیدوارم این مقاله برای شما مفید بوده باشد.