Веб
IDOR и ошибки авторизации: доступ к данным другого пользователя
Можно ли открыть чужой отчёт, поменяв число в URL? Что такое IDOR, как его найти и как закрыть с помощью policy в Laravel.
· 8 мин. чтения

Представьте: пользователь открывает свой счёт по адресу /invoices/1042. Затем меняет в адресной строке 1042 на 1041 — и видит счёт другой организации. Это IDOR (Insecure Direct Object Reference) — прямое обращение к объекту без проверки владельца.
IDOR — самое распространённое проявление категории A01 «Broken Access Control», которая занимает первое место в OWASP Top 10 (2021). В CWE это CWE-639 (Authorization Bypass Through User-Controlled Key). Автоматический сканер почти никогда его не находит: он не знает, какая запись кому принадлежит.
Аутентификация и авторизация — разные вещи
Корень многих ошибок — в смешении этих двух понятий:
- Аутентификация — «кто ты?» Вошёл ли пользователь в систему.
- Авторизация — «тебе это разрешено?» Может ли именно этот пользователь видеть или менять именно эту запись.
Middleware auth отвечает только на первый вопрос. Без второй проверки любой вошедший пользователь может отправить запрос с любым ID.
Где встречается IDOR
- Идентификаторы в URL:
/orders/15,/users/7/edit,/reports/2026-09.pdf?id=88. - API-запросы:
GET /api/documents/301,DELETE /api/comments/55. - Скрытые поля в формах:
<input type="hidden" name="user_id" value="7">. - Скачивание файлов:
/download?file=contract_15.pdf. - Параметр
organization_idв мультитенантных системах. - Страница возврата после оплаты:
/payment/result?order=124.
Последний случай особенно часто упускают. В статье о правильном процессе оплаты мы отдельно подчёркивали, что страница возврата должна открываться только владельцу заказа.
Пример ошибки в Laravel
public function show($id)
{
$invoice = Invoice::findOrFail($id);
return Inertia::render('Invoices/Show', ['invoice' => $invoice]);
}
Маршрут может находиться внутри middleware auth, но этот код вообще не проверяет, кому принадлежит запись.
Правильное решение: policy
В Laravel для хранения логики авторизации в одном месте используются policy:
php artisan make:policy InvoicePolicy --model=Invoice
class InvoicePolicy
{
public function view(User $user, Invoice $invoice): bool
{
return $invoice->organization_id === $user->current_organization_id;
}
public function update(User $user, Invoice $invoice): bool
{
return $this->view($user, $invoice)
&& $user->hasRole('accountant')
&& $invoice->status === 'draft';
}
}
В контроллере:
public function show(Invoice $invoice)
{
$this->authorize('view', $invoice);
return Inertia::render('Invoices/Show', ['invoice' => $invoice]);
}
Или на уровне маршрута:
Route::get('/invoices/{invoice}', [InvoiceController::class, 'show'])
->can('view', 'invoice');
Обратите внимание: в методе update помимо владения проверяются роль и статус записи. Авторизация — это не только вопрос «чьё это», но и вопрос «можно ли выполнить это действие в этом состоянии».
Ещё один слой: ограничение запроса по владельцу
В списках и поиске вместо проверки каждой записи через policy ограничьте сам запрос:
$invoices = $request->user()->currentOrganization
->invoices()
->latest()
->paginate(20);
Тогда записи другой организации вообще не извлекаются из базы. Для одной записи работает тот же приём: ->invoices()->findOrFail($id) вернёт 404 для чужого ID.
Правило: любой идентификатор от пользователя — это всего лишь запрос. Сервер каждый раз сам проверяет: «принадлежит ли запись этому пользователю и разрешено ли это действие?»
Типичные ошибки
- Скрытие только в интерфейсе. Даже если кнопки или ссылки не видно, запрос можно отправить вручную.
- Защищено только чтение.
showпроверяется, аupdateиdestroyзабыты. - Массовое обновление.
$invoice->update($request->all())может позволить пользователю поменять полеorganization_idилиuser_id. Явно задайте$fillableи используйте провалидированные данные. - Расчёт на UUID. Неугадываемые ID помогают, но не заменяют авторизацию: ID может утечь из другого места (лог, ссылка, экспорт).
- Связанные объекты. В
/projects/5/tasks/90проверяется проект, но не проверяется, что задача принадлежит этому проекту.
Как проверить
Самый эффективный способ — ручная проверка с двумя тестовыми аккаунтами:
- Создайте двух пользователей, по возможности в разных организациях.
- В первом аккаунте создайте записи и запишите их ID.
- Со второго аккаунта отправьте на эти ID запросы
GET,PUT,DELETE. - Каждый ответ должен быть 403 или 404, а не 200.
- Превратите проверки прав в автотесты, чтобы будущие изменения их не сломали:
it('не показывает счёт другой организации', function () {
$invoice = Invoice::factory()->create();
$stranger = User::factory()->create();
$this->actingAs($stranger)
->get("/invoices/{$invoice->id}")
->assertForbidden();
});
Итог
Технически IDOR прост, но чтобы его найти, нужно пройти каждый маршрут и найти в нём проверку владельца. В большом проекте это занимает много времени. В нашем процессе Claude AI агент быстро просматривает все маршруты, контроллеры и policy и выделяет места без проверок, а специалист на практике проверяет их с двумя аккаунтами и исправляет.
Если хотите убедиться, что в вашем проекте нет пути к чужим данным, оставьте заявку на сканирование. Мы поможем закрыть пробелы в авторизации до официальной экспертизы.


