Нарушение контроля доступа и IDOR: как найти и исправить
Практика4 минРедакция Innovialabs
Коротко
IDOR — это когда сервер отдаёт объект по ID, не проверив, принадлежит ли он текущему пользователю. Лечится проверкой прав на сервере для каждого запроса, выборками с фильтром по владельцу и автотестами вида «пользователь B не видит данные A».
Что такое IDOR
Broken Access Control — общее название ошибок, при которых пользователь может сделать то, на что у него нет прав. IDOR (Insecure Direct Object Reference) — самый частый её вид: сервер находит объект по идентификатору из запроса и отдаёт его, не проверив, чей он.
Классический пример. Пользователь открывает свой счёт:
GET /api/invoices/1042
Меняет число на 1041 — и видит чужой счёт с именем, адресом и суммой. Никакого взлома: обычный браузер и одна цифра.
То же самое работает для изменения и удаления (PUT /api/orders/77), загрузки файлов (/files/report-883.pdf), параметров в теле запроса ("user_id": 15) и скрытых полей формы.
Почему это риск номер один в OWASP Top 10
В OWASP Top 10 нарушение контроля доступа стоит на первом месте (A01). Причины практические:
- Фреймворк не сделает это за вас. Аутентификация подключается одной строкой, а правило «счёт видит только его владелец и бухгалтер компании» знает только ваш код.
- Проверку нужно повторить в каждом эндпоинте. Достаточно забыть её в одном, например в экспорте в CSV.
- Сканеры её почти не находят: они не знают, какие данные кому принадлежат.
- Фронтенд создаёт ложное чувство защиты: кнопки «Удалить» нет в интерфейсе, но API принимает запрос.
Виды нарушений
| Вид | Пример |
|---|---|
| Горизонтальная эскалация | клиент читает заказы другого клиента |
| Вертикальная эскалация | обычный пользователь вызывает /admin/users |
| Мультитенантность | сотрудник компании A видит данные компании B |
| Mass assignment | в теле запроса передают "role": "admin", и сервер его сохраняет |
| Обход через методы | GET проверен, а DELETE на тот же ресурс — нет |
Как исправить
Фильтруйте выборку по владельцу
Не «найти по ID, потом проверить», а сразу искать в пределах доступного пользователю:
// Плохо: отдаёт любой счёт
const invoice = await db.invoice.findUnique({ where: { id } });
// Хорошо: ищем только среди счетов текущего пользователя
const invoice = await db.invoice.findFirst({
where: { id, ownerId: session.userId },
});
if (!invoice) return res.status(404).end();
Для мультитенантных систем то же правило с tenantId в каждом запросе. В PostgreSQL можно добавить Row-Level Security как страховочный слой на уровне базы.
Централизуйте правила
Правила доступа не должны быть размазаны по контроллерам. Вынесите их в одно место — политики, middleware, функции вроде can(user, "edit", invoice). Так их проще читать, проверять на ревью и тестировать.
Запрещайте по умолчанию
Новый эндпоинт без явного правила должен быть закрыт, а не открыт. Роль и права берите из серверной сессии, а не из параметров запроса или JWT без проверки подписи.
Не путайте скрытие с защитой
- UUID вместо числовых ID усложняет перебор, но не заменяет проверку: ID утекают в ссылках, письмах и логах.
- Отсутствие кнопки в интерфейсе ничего не защищает.
- Разрешайте для записи только явный список полей, чтобы закрыть mass assignment.
Отвечать 404 вместо 403 для чужих объектов — нормальная практика: так атакующий не узнаёт, существует ли объект.
Как искать и тестировать
- Два пользователя, один сценарий. Создайте A и B, выполните действия от A, затем повторите запросы с сессией B. Каждый успешный ответ — находка.
- Матрица доступа. Таблица «роль × эндпоинт × метод» с ожидаемым результатом. Она же становится основой тестов.
- Автотесты на отказ. Для каждого ресурса — тест, что чужой пользователь получает
404или403на чтение, изменение и удаление. - Ревью кода. Ищите запросы к базе по ID без условия на владельца или тенанта.
- Логирование отказов. Всплеск
403/404от одного аккаунта — признак перебора.
it("user B cannot read user A's invoice", async () => {
const invoice = await createInvoice(userA);
const res = await api.as(userB).get(`/api/invoices/${invoice.id}`);
expect(res.status).toBe(404);
});
Подробные рекомендации — в OWASP Authorization Cheat Sheet.