خلاصه کلیدی
وقتی یک API خوب ساخته شده باشد، کلاس کاملی از آسیبپذیریها میتواند بدون هیچ خطای آشکاری پنهان بماند. تست پاسخمحور ناکافی است؛ باید نقشمحور باشد.
سالها است که ابزارهای امنیتی با یک منطق ساده کار میکنند: درخواست بفرست، پاسخ را با پاسخ درخواست قبلی مقایسه کن، و اگر تفاوتی بود، یافته ثبت کن. این منطق برای دهه ۲۰۰۰ عالی بود — وقتی اپلیکیشنها HTML و یک endpoint داشتند. اما در APIهای امروزی، این رویکرد دقیقاً همانجایی شکست میخورد که خطرناکترین باگها هستند.
یک تیم مهندسی خوب در سال ۲۰۲۶ وقتی شما فیلد `user_id` را عوض میکنید، خطای ۴۰۳ با پیام «شما به این منبع دسترسی ندارید» برمیگرداند. پاسخ کاملاً یکسان، با کد یکسان، برای درخواستهای نامعتبر و معتبر. هیچ تفاوتی وجود ندارد. اسکنر چیزی برای گزارش پیدا نمیکند — و هر چیزی که پیدا کند، نویز است.
ریشه مشکل: تعریف اشتباه «کار میکند»
IDOR یک باگ نیست؛ یک نبودِ کنترل دسترسی در جایی است که هیچکس تصمیم نگرفته کنترل لازم است. تیم توسعهدهنده، در زمان نوشتن تابع، یک تصمیم نگرفته — نه اینکه تصمیم گرفته اشتباه باشد. در تست، ما دنبال «پیام خطای اشتباه» نمیگردیم. ما دنبال جایی میگردیم که در آن جمله «آیا این درخواست مجاز است؟» هرگز پرسیده نشده.
این تفاوت ظاهری نیست. یک API با ده endpoint بدون کنترل دسترسی، ده برابر خطرناکتر از یک API با یک endpoint بدون کنترل — حتی اگر هر دو در اسکنر امنیتی یکسان به نظر برسند.
تست پاسخمحور در برابر تست نقشمحور
تست پاسخمحور از یک نشانه بیرونی استفاده میکند: طول پاسخ، کد وضعیت، یا تفاوت متن. تست نقشمحور از یک نشانه داخلی استفاده میکند: آیا این داده متعلق به من است؟
- برای هر endpoint، حداقل سه نقش تعریف کنید: کاربر عادی، کاربر دیگر (با داده متفاوت)، و مدیر.
- با نقش اول درخواست بفرستید و پاسخ موفق را ثبت کنید — این «خط پایه» شماست.
- همان درخواست را با هویت نقش دوم بفرستید. اگر داده برگشت، یافته دارید؛ حتی اگر کد پاسخ یکسان باشد.
- تفاوت مقدار ویژه (مثل نام، شناسه، یا محتوا) را مقایسه کنید، نه کل پاسخ را.
- همین چهار گام را با نقش سوم تکرار کنید تا ببینید مدیر چه چیزی میبیند که کاربر عادی نباید.
# ❌ response-based: the scanner sees nothing
GET /api/v2/invoices/100241 (as user A)
HTTP/1.1 403 Forbidden
{"error":"not_permitted"}
GET /api/v2/invoices/100241 (as user A, id changed)
HTTP/1.1 403 Forbidden
{"error":"not_permitted"}
# ✓ role-based: same codes, different reality
GET /api/v2/invoices/100241 (as user A)
HTTP/1.1 200 OK
{"id":100241,"amount":4200000,"holder":"A Corp"}
GET /api/v2/invoices/100241 (as user B, same id)
HTTP/1.1 200 OK
{"id":100241,"amount":4200000,"holder":"B Corp"} # someone else's moneyچرا این باگ در پروژههای ما تکرار میشود
در بیشتر پروندههایی که بررسی کردهایم، الگو یکسان است: یک لایه مدل (ORM/model layer) فیلتر مالکیت را اعمال میکند، اما چند endpoint جدید که مستقیم با query builder نوشته شدهاند، این لایه را دور میزنند. تیم توسعه فکر میکند دارد یک تابع مشترک را صدا میزند؛ در واقع، هر کدام منطق خودش را دارد.
مرز با BOLA و فرزندانش
OWASP این خانواده را BOLA مینامد: Broken Object Level Authorization. شناسه جایی است که شکاف رخ میدهد، اما ریشه همیشه یکی از سه چیز است: نبود بررسی مالکیت، نبود بررسی نقش، یا نبود اعتبارسنجی ورودی خودِ شناسه. این سه، در عمل، سه وصله متفاوت میخواهند — و تیمی که فقط اولی را وصله کند، دو مورد دیگر را باز گذاشته است.
- بررسی مالکیت: آیا رکورد به این شناسه در این فضای کاری تعلق دارد؟
- بررسی نقش: آیا این نقش مجاز به این عملیات روی این نوع منبع هست؟
- اعتبارسنجی شناسه: آیا مقدار، نوع درست را دارد و در محدوده معتبر است؟
اگر یافتهای را نمیتوانید برای دو نقش مختلف تکرار کنید، احتمالاً هنوز آن را درست متوجه نشدهاید.
این معیار ساده، در تقریباً هر ارزیابی که انجام میدهیم بیشترین تفاوت را ایجاد میکند. یک یافته که فقط با یک حساب پیدا شده، معمولاً یا مصنوعی است یا ناقص. یک یافتهای که با سه نقش و ده شناسه بازتولید میشود، یافتهای است که تیم شما واقعاً به آن اهمیت میدهد.