اولین بار که به IDOR برخوردم، در یک پروژه فروشگاهی کوچک بود. مشتری زنگ زد و با صدای نگران گفت که با تغییر یک عدد در آدرس مرورگر، فاکتور سفارش‌های دیگر را می‌بیند. آن شب تا صبح بیدار ماندم و کل کدبیس را زیر و رو کردم؛ در ده‌ها نقطه دیگر همین الگو تکرار شده بود: یک شناسه عددی در URL که هیچ‌کس چک نکرده بود آیا کاربر جاری مجاز به دیدنش هست یا نه. از آن روز، IDOR (Insecure Direct Object Reference) برای من از یک آیتم انتزاعی در لیست OWASP (Open Web Application Security Project) به یک الگوی ملموس در کد تبدیل شد.

IDOR دقیقاً چیست؟

IDOR یا ارجاع مستقیم ناامن به شیء، دسته‌ای از آسیب‌پذیری‌های کنترل دسترسی (Access Control) است که وقتی رخ می‌دهد که یک برنامه به‌جای بررسی اینکه کاربر جاری اجازه دسترسی به یک منبع را دارد یا نه، صرفاً به شناسه‌ای که در درخواست آمده اعتماد می‌کند. منبع می‌تواند یک رکورد دیتابیس، فایل، سفارش، پیام خصوصی یا هر چیز دیگری باشد.

در عمل، وقتی درسیستم یک عدد به‌عنوان شناسه رکورد در URL استفاده می‌شود (مثلاً example.com/orders/1234)، اگر سرور هنگام پردازش این درخواست بررسی نکند که آیا کاربر جاری مالک سفارش ۱۲۳۴ است یا نه، یک IDOR شکل گرفته است. کاربر مهاجم کافی است عدد را به ۱۲۳۵ تغییر دهد تا سفارش کاربر دیگری را ببیند.

IDOR در فهرست OWASP Top 10 در دسته Broken Access Control قرار می‌گیرد. این دسته در نسخه‌های اخیر این فهرست، رتبه اول را در بین ده آسیب‌پذیری خطرناک وب به خود اختصاص داده و IDOR یکی از شایع‌ترین اعضای همین خانواده است. اگر با مفهوم کلی آسیب‌پذیری وب چیست آشنا نیستید، پیشنهاد می‌کنم ابتدا آن مقاله را بخوانید و سپس به این بحث برگردید. همچنین انواع آسیب‌پذیری‌های رایج وب نقشه‌ای کلی از هم‌خانواده‌های IDOR به دست می‌دهد.

IDOR باگ رمزنگاری یا احراز هویت نیست؛ مشکل این است که سیستم، هویت را می‌شناسد اما اجازه را بررسی نمی‌کند.

چرا این آسیب‌پذیری این‌قدر شایع است؟

در تجربه‌ام، IDOR به چند دلیل در پروژه‌های وب این‌قدر زیاد دیده می‌شود. اول اینکه برخلاف XSS (Cross-Site Scripting) یا SQL Injection، برای شکل‌گیری IDOR نیازی به ورودی خاص یا کاراکتر مرموز نیست. کافی است یک عدد را در URL تغییر دهید. این سادگی، هم بهره‌برداری را آسان می‌کند، هم تشخیصش را دشوار — چون یک درخواست کاملاً نرمال و مشروع به نظر می‌رسد.

دلیل دوم، معماری‌های ناقص کنترل دسترسی است. در خیلی از پروژه‌ها، توسعه‌دهنده تمرکزش را روی احراز هویت (Authentication) می‌گذارد و فرض می‌کند اگر کاربر لاگین است، پس همه‌چیز مرتب است. اما کنترل دسترسی (Authorization) لایه‌ای جداست. اگر با تفاوت این دو لایه آشنا نیستید، مقاله CSRF چیست و چگونه دفع می‌شود هم به شکلی موازی همین تفکیک را بررسی می‌کند.

دلیل سوم، رشد سریع APIها است. وقتی یک تیم برای اپلیکیشن موبایل یک REST API (Representational State Transfer Application Programming Interface) می‌سازد، به‌ندرت اتفاق می‌افتد که همه endpointها به‌طور مستقل بررسی دسترسی داشته باشند. اگر در امنیت API در وب دقت کرده باشید، این نکته را برجسته کرده‌ام که کنترل دسترسی باید در هر endpoint تکرار شود، نه فقط در لایه احراز هویت.

انواع IDOR و سطوح حمله

IDOR فقط یک شکل ندارد. در عمل با چند سطح از این آسیب‌پذیری مواجه می‌شوم که شناختشان برای تشخیص بهتر ضروری است:

نوع IDORمثالسطح حمله
افقی (Horizontal)دیدن سفارش کاربر دیگر با تغییر IDهم‌سطح، بین کاربران عادی
عمودی (Vertical)دسترسی کاربر عادی به پنل مدیربالاسطح، بین سطوح دسترسی
مبتنی بر فایلدانلود مستقیم فایل با نام قابل حدسدسترسی به فایل‌های حساس
مبتنی بر پارامترتغییر user_id در بدنه درخواستداده‌های پنهان در payload
مبتنی بر GUID ضعیفحدس شناسه‌های ترتیبی UUID-likeسرشماری ساده

IDOR افقی و عمودی، دو روی یک سکه‌اند. در نوع افقی، مهاجم به داده‌های هم‌سطح خودش دست پیدا می‌کند: سفارش دیگری، پروفایل دیگری، فایل دیگری. در نوع عمودی، مهاجم از یک نقش پایین‌تر به یک نقش بالاتر می‌رسد: مثلاً کاربر عادی که با تغییر پارامتر به پنل ادمین راه پیدا می‌کند. اشتباهی که زیاد می‌بینم این است که تیم‌ها فقط روی IDOR افقی تمرکز می‌کنند و از عمودی غافل می‌شوند، در حالی که نوع عمودی به‌مراتب خطرناک‌تر است.

یک مثال واقعی از کد آسیب‌پذیر

بیایید یک مثال ساده ببینیم تا بحث از حالت انتزاعی بیرون بیاید. فرض کنید یک endpoint ساده در PHP داریم که جزئیات یک سفارش را برمی‌گرداند. نسخه آسیب‌پذیر معمولاً چنین شکلی دارد:

<?php
// Vulnerable: no ownership check
$order_id = (int) $_GET['id'];
$order = $wpdb->get_row(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}shop_orders WHERE id = %d",
        $order_id
    )
);
echo json_encode( $order );

در این کد، توسعه‌دهنده از $wpdb->prepare استفاده کرده که جلوی SQL Injection (SQLi) را می‌گیرد — کاری که درست است — اما هیچ بررسی‌ای نکرده که آیا کاربر جاری مالک سفارش $order_id هست یا نه. هر کاربری که عدد ۱۲۳۴ را به ۱۲۳۵ تغییر دهد، سفارش کاربر دیگری را می‌بیند. نکته تلخ اینجاست که این کد از دید یک بازبین سطحی، امن به نظر می‌رسد، چون از prepare استفاده شده است.

امن‌سازی ورودی‌ها، کنترل دسترسی نیست. این دو لایه باید جدا و مستقل از هم اجرا شوند.

همین الگو در زبان‌های دیگر هم تکرار می‌شود. در Django اگر از get_object_or_404 استفاده کنید ولی queryset را با کاربر جاری محدود نکنید، همان نتیجه را می‌گیرید. در Laravel اگر مدل را بدون authorize یا Policy برنگردانید، باز هم IDOR دارید. مساله فراتر از زبان و فریم‌ورک است؛ یک الگوی معماری است.

چگونه IDOR را در پروژه پیدا کنیم؟

برای پیدا کردن IDOR، من معمولاً یک رویکرد سیستماتیک دنبال می‌کنم که در چگونه آسیب‌پذیری سایت را پیدا کنیم به‌طور مفصل بازش کرده‌ام. خلاصه‌اش در پنج گام:

  1. فهرست همه endpointها را بسازید. هر جایی که یک شناسه در URL، بدنه درخواست یا هدر ارسال می‌شود، یک کاندید IDOR است.
  2. دو حساب کاربری بسازید. با یک حساب لاگین کنید، درخواست را با ابزاری مثل Burp Suite یا Postman بگیرید و با حساب دیگر امتحان کنید.
  3. شناسه‌ها را دستی تغییر دهید. ID سفارش، شناسه کاربر، نام فایل، پارامتر user_id در بدنه — همه را بالا و پایین کنید.
  4. شناسه‌های پنهان در HTML را بررسی کنید. گاهی ID دیگری در فرم مخفی یا در response یک API نامرتبط لو می‌رود.
  5. روی پاسخ‌ها دقت کنید. اگر با شناسه جعلی، پاسخ ۲۰۰ گرفتید، ممکن است IDOR باشد. اگر ۴۰۳ گرفتید، احتمالاً لایه کنترل دسترسی سالم است — ولی گاهی هم پیام خطا جزئیات لو می‌دهد.

اگر می‌خواهید این کار را با ابزار خودکار جلو ببرید، فهرست اسکنرهای آسیب‌پذیری وب نقطه شروع خوبی است — با این تذکر که هیچ اسکنری جای تست دستی IDOR را نمی‌گیرد، چون شناخت منطق کسب‌وکار لازم است.

راه‌حل‌های عملی برای رفع IDOR

خبر خوب این است که IDOR یکی از قابل‌رفع‌ترین آسیب‌پذیری‌هاست. خبر بد این است که رفع سطحی، خطر را جابه‌جا می‌کند و آن را پنهان می‌سازد. راه‌حل واقعی، چند لایه دارد:

۱. بررسی مالکیت در هر درخواست

اولین لایه، اضافه کردن یک شرط ساده به کوئری است. نسخه اصلاح‌شده کد بالا:

<?php
$order_id    = (int) $_GET['id'];
$current_id  = get_current_user_id();
$order = $wpdb->get_row(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}shop_orders
         WHERE id = %d AND user_id = %d",
        $order_id, $current_id
    )
);
if ( ! $order ) {
    status_header( 404 );
    exit;
}
echo json_encode( $order );

نکته کلیدی این است که شرط user_id هم داخل همان کوئری باشد، نه اینکه بعد از خواندن رکورد چک شود. اگر رکورد را خواندید و بعد بررسی کردید، احتمال لو رفتن اطلاعات در لاگ یا حافظه وجود دارد.

۲. استفاده از Policy یا Capability

در وردپرس، current_user_can() ابزار استاندارد بررسی دسترسی است. برای هر اکشن، یک capability تعریف کنید و در ابتدای هندلر بررسی کنید:

if ( ! current_user_can( 'edit_order', $order_id ) ) {
    wp_die( 'Access denied', 403 );
}

در فریم‌ورک‌های دیگر هم ابزارهای مشابهی وجود دارد: Policy در Laravel، Permission در Django REST Framework، Authorize attribute در ASP.NET. مهم این است که بررسی دسترسی، بخشی از جریان اصلی کد باشد، نه یک گزینه اضافه.

۳. شناسه‌های غیرقابل حدس

یک لایه دفاعی مکمل، جایگزین کردن شناسه‌های عددی با UUID (Universally Unique Identifier) یا ULID است. این کار IDOR را کاملاً رفع نمی‌کند — چون همچنان مهاجم می‌تواند شناسه دیگری را از منبعی پیدا کند — اما هزینه سرشماری و حمله جستجو-محور را به‌شدت بالا می‌برد. برای داده‌های حساس مثل فایل‌های خصوصی یا توکن‌های اشتراک، این لایه ارزشش را دارد.

۴. ثبت و پایش تلاش‌های شکست‌خورده

وقتی دسترسی رد می‌شود، آن را لاگ کنید. تکرار تلاش‌های رد‌شده از یک IP یا یک حساب کاربری، سیگنال واضحی از تلاش برای IDOR است. این لاگ‌ها به شما کمک می‌کنند هم حادثه را زودتر ببینید، هم بعد از رفع، مطمئن شوید مشکل جدی نبوده است.

IDOR در APIها: چالش بزرگ‌تر

در APIها، IDOR شکل خطرناک‌تری می‌گیرد چون هیچ لایه نمایشی جلوی آن نیست. اگر در یک اپلیکیشن وب، نمایش سفارش کاربر دیگر نیاز به HTML و رندر داشته باشد، در API یک JSON ساده کافی است. مهاجم می‌تواند داده‌ها را با سرعت بسیار بالا استخراج کند.

سه اشتباه رایج در APIها که IDOR می‌سازند:

  • JWT (JSON Web Token) را به‌عنوان کنترل دسترسی در نظر گرفتن. JWT فقط هویت را منتقل می‌کند؛ این‌که کاربر اجازه دیدن این منبع خاص را دارد یا نه، مسئله‌ای جدا است. برای تفاوت این دو، احراز هویت در API را ببینید.
  • بررسی دسترسی در لایه میان‌افزار به‌جای endpoint. اگر فقط چک کنید که کاربر لاگین است ولی نگویید به چه منابعی دسترسی دارد، IDOR شکل می‌گیرد. در بهترین روش‌های امنیت API این موضوع را بیشتر باز کرده‌ام.
  • اعتماد به پارامترهای سمت کلاینت. هیچ‌وقت به user_id که در بدنه درخواست می‌آید اعتماد نکنید؛ همیشه از توکن احراز هویت بگیرید.

IDOR در وردپرس و افزونه‌ها

وردپرس ذاتاً ساختار مناسبی برای کنترل دسترسی دارد: نقش‌ها، capabilities و توابعی مثل current_user_can و wp_verify_nonce. اما در عمل، IDOR در افزونه‌ها و قالب‌های سفارشی زیاد دیده می‌شود. الگوی رایج این است که یک endpoint با admin-ajax.php یا REST API ثبت می‌شود، ولی فقط is_user_logged_in چک می‌شود و نه مالکیت منبع.

نکته‌ای که بارها در بازبینی افزونه‌های وردپرسی دیده‌ام: حتی اگر افزونه اصلی IDOR نداشته باشد، افزونه‌های جانبی که همان endpoint را توسعه می‌دهند می‌توانند یک حفره جدید باز کنند. اگر با مفهوم آسیب‌پذیری افزونه‌های وردپرس آشنا هستید، می‌دانید که این مساله چقدر رایج است. برای مدیریت سیستماتیک این دسته از آسیب‌پذیری‌ها، راهنمای اولویت‌بندی و رفع آسیب‌پذیری‌ها چارچوب عملی خوبی می‌دهد.

اشتباهات رایج در رفع IDOR

  • مخفی کردن دکمه به‌جای حذف دسترسی. اگر با CSS یا JavaScript دکمه ویرایش را برای کاربر عادی پنهان می‌کنید ولی endpoint همچنان باز است، IDOR رفع نشده. یک مهاجم با ابزار HTTP، دکمه‌ای که نمی‌بیند را هم صدا می‌زند.
  • بررسی دسترسی فقط در front-end. همان اشتباه بالا با یک شکل دیگر. کنترل دسترسی باید در سرور باشد؛ front-end قابل دست‌کاری است.
  • پیام خطای گویا. اگر با شناسه نامعتبر پیام سفارش متعلق به شما نیست برگردانید، به مهاجم می‌گویید که آن سفارش وجود دارد ولی مال شما نیست. این خودش یک نشت اطلاعات است. پیام ۴۰۴ عمومی بدهید.
  • رها کردن رکوردهای یتیم. گاهی توسعه‌دهنده فکر می‌کند اگر کاربر منبع را ببیند ولی نتواند تغییر دهد، مشکلی نیست. تماشا هم نشت اطلاعات است.

این‌ها همان اشتباهاتی هستند که در اشتباهات رایج در مدیریت آسیب‌پذیری‌ها هم به آن‌ها پرداخته‌ام، ولی در بستر IDOR شکل ملموس‌تری می‌گیرند.

نگاه لایه‌ای به ریشه مشکل

برای مهندسانی که با معماری سیستم‌های بزرگ سروکار دارند، IDOR یک باگ نقطه‌ای نیست — نشانه‌ای از یک ضعف ساختاری در طراحی لایه کنترل دسترسی است. چند مشاهده دقیق‌تر که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

اول، مرز میان احراز هویت و مجوزدهی (Authorization) در سیستم‌های توزیع‌شده مبهم می‌شود. اگر سرویس A هویت را از JWT می‌خواند و سرویس B تصمیم دسترسی را می‌گیرد، به‌ندرت اتفاق می‌افتد که این دو سرویس یک مدل داده مشترک از مالکیت منابع داشته باشند. نتیجه این می‌شود که در یکی از مرزها، یک فرض نادرست درباره اینکه طرف مقابل دسترسی را بررسی کرده، شکل می‌گیرد.

دوم، در معماری‌های multi-tenant (چند-مستأجری)، IDOR به یک کلاس کامل از آسیب‌پذیری تبدیل می‌شود. اگر جداکننده مستأجرها (Tenant Isolation) در لایه دیتابیس تضمین نشود و فقط در کد اپلیکیشن فرض شود، هر نشت یا فراموشی در کد به نشت اطلاعات بین مستأجرها منجر می‌شود. الگوی درست، استفاده از Row-Level Security در پستگرس یا معادل‌های آن در سایر دیتابیس‌ها است، نه اعتماد به بازبینی کد در هر کوئری.

سوم، از دید آزمون نفوذ مدرن، IDOR یکی از مواردی است که ابزارهای خودکار در آن ضعیف عمل می‌کنند. ابزارها می‌توانند IDORهای ساده را پیدا کنند، اما IDORهایی که بر پایه منطق کسب‌وکار شکل گرفته‌اند — مثلاً ترکیب چند پارامتر که فقط با هم یک منبع حساس را نشان می‌دهند — نیازمند تحلیل انسانی هستند. به همین دلیل، سرمایه‌گذاری روی تست دستی IDOR در پروژه‌های حساس، بازده بالاتری از خرید اسکنر گران‌قیمت دارد.

چهارم، در سیستم‌هایی که از GraphQL استفاده می‌کنند، IDOR شکل متفاوتی می‌گیرد. چون GraphQL اجازه می‌دهد کلاینت شکل کوئری را تعیین کند، حلقه‌های تودرتوی ارتباطات (مثل سفارش - مشتری - آدرس) می‌توانند به یک بردار حمله تبدیل شوند اگر resolverها به‌طور مستقل دسترسی را بررسی نکنند. اینجا مسئولیت کنترل دسترسی از سطح HTTP به سطح نوع داده منتقل می‌شود و اگر تیم این تفاوت را نداند، احتمال بروز IDOR چند برابر می‌شود.

خط پایان

IDOR همان‌قدر که ساده به نظر می‌رسد، می‌تواند خطرناک باشد. اگر بخواهم کل این مقاله را در سه نکته خلاصه کنم: اول اینکه IDOR یک باگ نقطه‌ای نیست، یک ضعف در طراحی لایه کنترل دسترسی است؛ دوم اینکه رفعش نیاز به بررسی مالکیت منبع در هر درخواست دارد، نه فقط در لایه احراز هویت؛ و سوم اینکه هیچ ابزار خودکاری جای بازبینی سیستماتیک endpointها و منطق کسب‌وکار را نمی‌گیرد.

اگر امروز فقط یک کار می‌توانید بکنید، این باشد: فهرستی از همه endpointهایی که در URL یا بدنه درخواست یک شناسه می‌گیرند تهیه کنید و ببینید در کدام‌یک از آن‌ها شرط مالکیت یا بررسی capability وجود دارد. همین یک بازبینی ساده، معمولاً پرونده چند IDOR پنهان را باز می‌کند.

اگر در پروژه‌ای با IDOR مواجه شده‌اید، برایم جالب است بدانید کدام لایه معماری بیشترین زمان را از شما گرفت — احراز هویت، مجوزدهی، یا منطق کسب‌وکار. تجربه‌تان را در دیدگاه بنویسید؛ به‌خصوص اگر در پروژه‌ای از معماری‌های چند-مستأجری یا GraphQL استفاده کرده‌اید، راه‌حل شما برای نفر بعدی راهگشاست. 🔐