کاربر زنگ میزند یا تیکت میگذارد: «گوشیام را عوض کردم، کد دومرحلهای نمیآید، حساب را باز کنید.» شما پشت پنل نشستهاید و باید در چند دقیقه تصمیم بگیرید. همین لحظه است که معلوم میشود مسیر بازیابی حساب کاربری شما طراحیشده است یا فقط یک دکمه «بازنشانی توسط ادمین» که هر کسی با یک ایمیل میتواند از آن سوءاستفاده کند. اگر آن دکمه بیقاعده باشد، تمام زحمتی که برای 2FA کشیدهاید صفر میشود؛ مهاجم لازم نیست کد را بشکند، فقط باید پشتیبانی را قانع کند.
چرا مسیر بازیابی حساب کاربری نقطه ضعف 2FA است
احراز دومرحلهای فرض میکند که عامل دوم (گوشی، اپلیکیشن TOTP، کلید سختافزاری) در اختیار کاربر است. اما هر سیستم بازیابی، یک استثنا روی همین فرض میسازد: «اگر عامل دوم نبود، چه کنیم؟» و مهاجم دقیقاً روی همین استثنا کار میکند. به این حمله در ادبیات امنیت میگویند MFA fatigue یا فرسایش دومرحلهای؛ یعنی بهجای شکستن رمز، روی انسان پشتیبانی فشار میآورند.
سه الگوی شکست را در عمل زیاد دیدهام:
- بازنشانی با ایمیل پشتیبان: کاربر ایمیل بازیابی را سالها قبل ثبت کرده و همان ایمیل هم در نشت دادهها لو رفته. مهاجم با آن ایمیل، درخواست بازیابی میزند و کد به صندوق خودش میرسد.
- بازنشانی با شماره تلفن: اگر شماره را از پروفایل کاربر میخوانید و شماره تأییدنشده است، مهاجم شماره خودش را ثبت میکند و بعد بازیابی میزند.
- بازنشانی دستی توسط ادمین: بدترین حالت. یک اپراتور خسته، ساعت ۳ بامداد، با یک اسکرینشات جعلی از پنل کاربر، رمز را عوض میکند.
نکتهای که خیلیها از دست میدهند این است: بازیابی حساب کاربری یک مسیر احراز هویت است، نه یک فرایند پشتیبانی. باید مثل ورود، لاگ داشته باشد، محدودیت نرخ داشته باشد و قابل ممیزی باشد. اگر لاگ ممیزیتان فقط «ورود موفق» را ثبت میکند و «بازنشانی رمز توسط ادمین» را نه، شما در واقع کور هستید. در لاگ ممیزی چیست و چگونه آن را دستنخورده نگه داریم؟ درباره اینکه چه فیلدهایی را باید ثبت کنید و چطور لاگ را از دستکاری اپراتور محافظت کنید، مفصل نوشتهام.
کد بازیابی: چطور ذخیره و چطور باطل شود
کد بازیابی (recovery code) باید یکبارمصرف و هششده ذخیره شود، دقیقاً مثل رمز عبور. اگر کد را بهصورت plaintext در ستون recovery_code نگه میدارید، یک SQL Injection ساده کل حسابها را باز میکند. الگوی درست این است:
-- ساختار پیشنهادی
CREATE TABLE recovery_codes (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
code_hash CHAR(60) NOT NULL, -- bcrypt
used_at DATETIME NULL,
created_at DATETIME NOT NULL,
INDEX idx_user (user_id)
);
هنگام تولید، کد را با bcrypt یا argon2id هش کنید و فقط یکبار به کاربر نشان دهید. تعداد کدها را محدود کنید؛ ده کد کافی است. هر کد که استفاده شد، used_at پر شود و دیگر قابل استفاده نباشد. اگر کاربر همه کدها را گم کرد، مسیر باید برود سراغ تأیید هویت انسانی، نه بازنشانی خودکار.
یک تله رایج: کد بازیابی را با md5 یا sha1 هش میکنند چون «کد طولانی است». طول کد ربطی به امنیت هش ندارد. کد بازیابی معمولاً ۱۶ کاراکتر است و فضای جستوجویش کوچک؛ بدون salt و الگوریتم کند، با یک GPU در چند ساعت شکسته میشود.
اینجا اشتباه میکنند
اشتباه واقعی که زیاد دیدهام: تیم امنیت کد بازیابی را درست پیاده میکند، اما در فرم ورود یک گزینه «فراموشی رمز» میگذارد که مستقیم با ایمیل، رمز را عوض میکند و 2FA را دور میزند. یعنی کاربری که 2FA دارد، با کلیک روی «فراموشی رمز» و دسترسی به ایمیل، بدون کد دومرحلهای وارد میشود. علامتش هم این است: در لاگها میبینید کاربرانی که 2FA فعال دارند، گاهی بدون ثبت رویداد دومرحلهای وارد شدهاند. اگر این را دیدید، بدانید که 2FA شما تزئینی است.
تأیید هویت پشتیبانی: چه چیزی قابل جعل نیست
وقتی کاربر همه راهها را بسته، آخرین خط دفاعی تأیید هویت انسانی است. اینجا باید سؤالهایی بپرسید که در نشت دادهها نباشند. تاریخ تولد، کد ملی، آخرین چهار رقم کارت و نام مادر، همه در نشتهای ایرانی بارها دیده شدهاند و ارزش اثباتی ندارند.
سؤالهایی که واقعاً کار میکنند، از دادههای داخلی سیستم میآیند که مهاجم به آنها دسترسی ندارد:
- تاریخ آخرین ورود موفق به حساب (با اختلاف چند روز قابل قبول).
- نام دو دامنه یا دو پروژهای که کاربر ساخته، بدون اینکه در پنل نمایش داده شود.
- آخرین مبلغ فاکتور پرداختشده و روش پرداخت.
- یک رویداد اخیر که فقط در لاگ داخلی است، مثل «تغییر DNS در تاریخ مشخص».
اگر کاربری نمیتواند به اینها جواب دهد، بازیابی را رد کنید. بله، ممکن است یک مشتری واقعی ناراضی شود. اما هزینه یک حساب دزدیدهشده، بهمراتب بیشتر از یک تیکت ناراضی است. اینجا جایی است که باید موضع داشته باشید، نه اینکه با «حالا اینبار بازش میکنیم» از زیرش در بروید.
محدودیت نرخ و قفل تدریجی
مسیر بازیابی بدون rate limit، یک oracle برای تست رمز است. حداقل اینها را اعمال کنید:
| رویداد | محدودیت پیشنهادی | واکنش |
|---|---|---|
| درخواست کد بازیابی | ۳ بار در هر ۱۵ دقیقه | قفل موقت + ایمیل هشدار |
| ورود کد اشتباه | ۵ بار | باطل کردن همه کدها |
| بازنشانی توسط ادمین | حداکثر ۲ بار در روز برای هر اپراتور | نیاز به تأیید نفر دوم |
مورد سوم را جدی بگیرید. اگر یک اپراتور بتواند بینهایت بار رمز کاربران را بازنشانی کند، شما یک در پشتی انسانی ساختهاید. قانون دونفره (four-eyes) اینجا گران نیست؛ فقط یک تأیید اضافه است.
وقتی کاربر واقعاً دسترسیاش را از دست داده
حالت سخت: کاربر نه کد بازیابی دارد، نه به ایمیل دسترسی دارد، نه به شماره. اینجا دو انتخاب دارید. اول، بازیابی کامل با تأیید هویت انسانی و ثبت کامل در لاگ. دوم، رد کردن و پیشنهاد ساخت حساب جدید با انتقال داده توسط خود کاربر. من اولی را انتخاب میکنم، اما فقط اگر لاگ ممیزی دستنخورده و غیرقابلویرایش باشد و تأیید دونفره اجباری باشد. اگر این دو شرط نیست، دومی را انتخاب کنید؛ ریسک کمتر است.
یک نکته عملی: اگر روی زیرساخت خودتان سرویس میدهید، بخشی از این کار به لایه شبکه برمیگردد. حملههای credential stuffing که مسیر بازیابی را هدف میگیرند، معمولاً از چند IP مشخص میآیند و با محافظت در برابر DDoS و فیلتر نرخ در لبه شبکه قابل مهار هستند. همچنین برای بررسی اینکه درخواستها از کجا میآیند، ابزارهای بررسی DNS و شبکه کمک میکنند تا بفهمید آیا ترافیک از یک ASN خاص میآید یا نه.
چه چیزی را همین امروز عوض کنید
اگر فقط یک کار میتوانید انجام دهید، این است: هر مسیری که بدون عامل دوم، رمز را عوض میکند، باید یا حذف شود یا پشت تأیید هویت انسانی با لاگ اجباری برود. بعد از آن، کدهای بازیابی را هش کنید و rate limit بگذارید. اگر تیم شما هنوز روی رمز عبور مشترک کار میکند، اول مدیریت رمز عبور در تیمها را بخوانید؛ چون بازیابی حساب وقتی معنا دارد که رمزها از ابتدا درست مدیریت شوند.
و اگر روی وردپرس کار میکنید، یادتان باشد که REST API اغلب یک مسیر بازیابی غیررسمی است. در امنسازی REST API وردپرس توضیح دادهام چطور جلوی نشت لیست کاربران را بگیرید، چون همان لیست، نقطه شروع حمله به مسیر بازیابی است.
پرسشهای پرتکرار
آیا کد بازیابی باید هش شود یا رمزنگاری؟
هش، و از نوع کند مثل bcrypt یا argon2id. رمزنگاری فقط وقتی لازم است که بخواهید کد را دوباره به کاربر نشان دهید، که در مورد کد بازیابی اصلاً نباید این کار را بکنید. کد یکبارمصرف است و بعد از نمایش اول، دیگر نیازی به خواندنش نیست.
چند کد بازیابی کافی است؟
ده کد کافی است. بیشتر از این، سطح حمله را بدون فایده واقعی بزرگ میکند. مهمتر از تعداد، این است که هر کد فقط یکبار قابل استفاده باشد و بعد از پنج تلاش اشتباه، همه کدها باطل شوند.
اگر کاربر به ایمیل و شماره دسترسی نداشت چه کنیم؟
باید از تأیید هویت انسانی با سؤالهای داخلی استفاده کنید و کل فرایند را در لاگ ممیزی ثبت کنید. اگر امکان تأیید دونفره ندارید، بهتر است بازیابی را رد کنید و به کاربر بگویید حساب جدید بسازد و دادهاش را منتقل کند.
آیا میتوان بازیابی را کاملاً خودکار کرد؟
نه، اگر 2FA جدی میخواهید. هر بازیابی خودکار، یک استثنا روی 2FA است و مهاجم روی همان کار میکند. بازیابی خودکار فقط وقتی قابل قبول است که عامل دوم دیگری (مثل کلید سختافزاری پشتیبان) در دسترس باشد.
اگر میخواهید مطمئن شوید لایههای زیرین سرویستان در وضعیت پایدار است، وضعیت لحظهای سرویسها را چک کنید و برای جزئیات فنی به مستندات و پایگاه دانش سر بزنید. برای تیمهایی که میخواهند این کنترلها را از ابتدا درست بچینند، خدمات امنیت سرورنت نقطه شروع منطقی است.
دیدگاهها ۰
هنوز دیدگاهی ثبت نشده؛ اولین نفر باشید!