
مدتی پیش از طرف Github یه نوتیفیکیشن گرفتم درباره اینکه ورژن جنگوی یکی از پروژه هام یک باگ امنیتی داره که توضحاتش توی این صفحه هست. این باگ میتونه مشکلی توی سیستم بازیابی پسورد ایجاد بکنه و به حمله کننده اجازه بده تا پسورد کاربر رو عوض کنه. توی این مطلب درباره این باگ (که بار اولی نبود که دیده میشد و توی گیتهاب هم بود) توضیح میدم و سورس جنگو رو هم برای اینکه بهتر درک کنیم این باگ چطور بوجود اومده و راه برطرف کردنش چی بوده نگاه میکنیم.
فرض کنیم کاربری با ایمیلی به آدرس ali@domain.example توی سایت هست، حمله کننده برای بازیابی پسوورد از ایمیل alı@domain.example استفاده میکنه؛ و توی کد این دوتا ایمیل با هم مساوی حساب میشن! و در نتیجه ایمیل اشتباهی به ایمیل alı@domain.example ارسال میشه.
این باگ توی بخش reset password جنگو اتفاق میوفته، حمله کننده(که ایمیل قربانی رو میدونه) توی فرم reset password ایمیلی رو وارد میکنه که جنگو فکر میکنه ایمیل قربانی هست اما درواقع ایمیلی هست که حمله کننده به اون دسترسی داره و جنگو لینک عوض کردن پسوورد رو به ایمیل اشتباه میفرسته.
ایمیل ها توی دیتابیس معمولا به یه شکل(حروف کوچک یا بزرگ) ذخیره میشوند و وقتی که میخوایم ببینیم یه ایمیل توی دیتابیس هست یا نه راهش اینه که اون ایمیل رو بدون حساسیت به بزرگ و کوچک بودن پیدا کنیم، تا اینجا این کار درست هست اما قسمتی وجود داره که باید بهش توجه بشه. بعضی کاراکتر ها هستند که وقتی به حروف کوچیک تبدیل میشوند با هم مساوی میشوند. درواقع تبدیل یه کاراکتر به حروف کوچیک(یا بزرگ) رو میشه به شکل یه تابع هش در نظر گرفت که یه سری collision داره و این collision ها باعث یکی شدن دوتا ایمیل متفاوت میشن، برای مثال جواب این شرط همیشه True هست.
"i".upper() == "ı".upper() # True
اینجا میتونید یه لیست از کاراکتر هایی که توی بزرگ یا کوچیک شدن یکی میشن رو ببینید.
برای فرستادن ایمیل بازیابی پسوورد سه تا راه وجود داره:
۱- ایمیل رو به آدرسی که توی دیتابیس ذخیره کردیم(ایمیل اصلی کاربر) بفرستیم
۲- ایمیل رو به ایمیلی که حمله کننده وارد میکنه و به صورت uppercase یا lowercase درآوردیم بفرستیم
۳- ایمیل رو به ایمیلی که حمله کننده وارد کرده و تغییری توش ندادیم بفرستیم
راه حل اول به نظر راه درست هست و جنگو هم همین کار رو انجام داده تا جلوی این حمله رو بگیره.
این کامیتی هست که این باگ رو برطرف کرده و بهش نگاه بندازیم:
active_users = UserModel._default_manager.filter(**{ '%s__iexact' % email_field_name: email, 'is_active': True, })
توی این خط جنگو ایمیلی که برای بازیابی پسوورد ایمیل فرستاده شده رو بدون درنظر گرفتن بزرگی و کوچیکی حروف(case insensitive) توی دیتابیس پیدا میکنه.
در ادامه جنگو قبلا از همین متغیر email استفاده میکرده تا ایمیل بازیابی پسوورد رو بفرسته اما توی این کامیت این خط عوض شده و ایمیل به ایمیلی که کاربر توی دیتابیس داره فرستاده میشه:
self.send_mail(subject_template_name, email_template_name, context, from_email, - email, html_email_template_name=html_email_template_name, + user_email, html_email_template_name=html_email_template_name, )
توضیح: توی کد جدید به جای email از user_email استفاده شده که ایمیل توی دیتابیس هست.
پس اگر از این ورژن ها استفاده میکنید جنگو رو اپدیت کنید. :)
این مشکل فقط برای جنگو پیش نیومده و قبلا هم توی گیت هاب هم بوده؛ نتیجه این هست که همیشه موقع کار با string ها باید مراقب بود و بدونیم که چیکار میکنیم.
لینک توضیح کشف این باگ در سایت bug bounty گیت هاب.
تابحال روی یک مشکل امنیتی دقیق نشده بودم ولی از روی همین باگ که با چند خط تغییر رفع میشد چیزهایی یادگرفتم که بلد نبودم. اگر نظری برای بهتر شدن نوشته دارید توی کامنت بنویسید یا اگر از اون خوشتون اومده ❤️ کنید.