Что это
Контроль доступа (access control) отвечает на вопрос: имеет ли право именно этот пользователь выполнить именно это действие с именно этим ресурсом? Аутентификация подтверждает, кто ты (логин и пароль), а авторизация решает, что тебе можно. Нарушение контроля доступа (Broken Access Control) — это когда система пускает пользователя туда, куда ему нельзя: к чужим заказам, к админ-панели, к чтению чужого файла.
Аналогия: представьте отель, где на дверях есть замки, но горничная по привычке открывает любую дверь любым ключом, потому что «постоялец же уже зашёл в здание через ресепшен». Войти в отель (аутентификация) и иметь право войти в конкретный номер (авторизация) — разные вещи.
В редакции OWASP Top 10 за 2021 год этот класс поднялся на первое место: уязвимости находили в 94% протестированных приложений. Причина популярности проста — проверку прав легко забыть. Разработчик добавляет новый эндпоинт, тестирует его под своей учёткой, видит, что данные приходят, и идёт дальше. Что произойдёт, если подставить чужой идентификатор, он не проверяет.
Первопричина
Глубинная причина почти всегда одна: сервер доверяет тому, что прислал клиент, и не перепроверяет права на каждый ресурс самостоятельно. Браузер показал пользователю только его заказы — значит, рассуждает разработчик, чужой заказ он и не запросит. Но злоумышленник не пользуется браузером как обычный человек: он напрямую формирует HTTP-запросы и подставляет любые значения.
Типичная ошибка: прятать кнопку «Удалить» от обычных пользователей на фронтенде и считать, что эндпоинт удаления защищён. Скрытие элемента интерфейса — это удобство, а не безопасность. Запрос DELETE /api/users/42 отправит и тот, у кого кнопки нет.
Мини-итог: контроль доступа — это серверная проверка права на конкретный ресурс при каждом обращении; она ломается, когда сервер верит клиенту на слово, и поэтому стабильно держит первое место в OWASP Top 10.
IDOR — небезопасные прямые ссылки на объекты
IDOR (Insecure Direct Object Reference) — самый частый сценарий. Приложение использует идентификатор объекта прямо из запроса, не проверяя, принадлежит ли объект текущему пользователю. Вы заходите в свой заказ по адресу /api/orders/123. Меняете число на 124 — и видите чужой заказ с адресом доставки и телефоном другого человека.
Аналогия: гардеробные номерки идут по порядку. Если гардеробщик выдаёт пальто по номеру, не сверяя лицо, любой назовёт чужой номер и заберёт чужое пальто.
Подбор бывает прямым (последовательные числовые id) и не таким очевидным, когда id зашит в скрытое поле формы, в cookie или в JWT. Замена UUID на случайный почти невозможна перебором, но это не защита — это лишь усложнение. Если ссылка утечёт в лог, в реферер или в общий доступ, объект снова открыт. Настоящая защита — проверка владельца, а не непредсказуемость id.
Path traversal — выход за пределы каталога
Если приложение отдаёт файлы по имени из запроса (/download?file=report.pdf), злоумышленник подставляет последовательности ../, чтобы выбраться из разрешённой папки: /download?file=../../../../etc/passwd. Каждый ../ поднимает на каталог вверх, пока путь не упрётся в системные файлы. Встречаются и обходы фильтров: URL-кодирование (%2e%2e%2f), двойное кодирование, абсолютные пути.
Доверие скрытым полям и ролям с клиента
Ещё один класс — когда права или принадлежность передаются с клиента и принимаются как есть. Форма редактирования профиля шлёт role=user, и злоумышленник правит её на role=admin. Или в теле запроса есть userId, и сервер сохраняет данные под чужим идентификатором (mass assignment). Cookie вида isAdmin=false правится на true.
Типичная ошибка: хранить роль или признак владельца в редактируемом клиентом месте (скрытое поле, cookie, тело запроса) и читать оттуда при проверке прав. Источником истины должна быть серверная сессия или подписанный токен, сверенный с базой.
Мини-итог: IDOR ломает горизонтальную границу (чужие объекты того же уровня), path traversal вырывается из песочницы файловой системы, а доверие клиентским полям отдаёт врагу управление ролями — всё это варианты одной ошибки доверия вводу.
Вертикальная и горизонтальная эскалация
Эскалацию привилегий принято делить на два вектора. Горизонтальная — доступ к ресурсам пользователя того же уровня: обычный клиент читает заказы другого обычного клиента (классический IDOR). Вертикальная — повышение уровня прав: рядовой пользователь получает доступ к функциям администратора, например к /admin/users или к эндпоинту смены чужого пароля.
Вертикальная эскалация часто живёт в незащищённых административных URL. Разработчик убрал ссылку на админку из меню обычных пользователей, но сам путь /admin/panel отвечает всем, кто его знает (это называют forced browsing — насильственный обход по угаданным адресам). Проверка роли на эндпоинте просто забыта.
Аналогия: горизонтальная эскалация — это зайти в соседний номер того же этажа; вертикальная — подняться на закрытый этаж для VIP, потому что лифт не спрашивает карту.
Как защититься — бьём в первопричину
Лечится не симптом, а корень: сервер обязан сам, не доверяя клиенту, проверять право на каждый ресурс.
- Авторизация на сервере по каждому ресурсу. При каждом обращении сверяйте: владеет ли текущий пользователь (из сессии/токена) запрошенным объектом и разрешена ли ему операция. Для
/api/orders/123— этоWHERE order.id = 123 AND order.owner_id = currentUser.id. - Deny by default. Доступ закрыт по умолчанию; открывается только явным правилом. Новый эндпоинт без правила должен отвечать
403, а не пускать всех. - Идентичность — только из серверного контекста. Кто пользователь и какая у него роль — берём из сессии или проверенного на сервере токена, никогда из тела запроса, cookie или скрытого поля.
- Централизованный механизм. Одна общая прослойка проверки прав вместо копипасты в каждом обработчике — меньше шансов забыть.
- Path traversal: канонизируйте путь и проверяйте, что он остаётся внутри разрешённого каталога; лучше — выдавайте по белому списку идентификаторов, а не по имени файла из запроса.
- Логирование отказов доступа и алерты на всплески
403— ранний признак перебора id.
Типичная ошибка: полагаться на «непредсказуемые» UUID вместо проверки владельца. Это безопасность через неясность; реальной границы она не создаёт.
Мини-итог: защита от A01 — это серверная авторизация по каждому ресурсу с принципом deny by default и идентичностью строго из серверного контекста; всё остальное (скрытие кнопок, случайные id) — лишь декорация.
# Атакующий просто меняет id в адресе на чужой
GET /api/orders/123 HTTP/1.1
Host: shop.example.com
Cookie: session=eyJ1c2VyIjo3fQ... # сессия пользователя #7
# Уязвимый бэкенд (псевдокод): берёт заказ по id, НЕ проверяя владельца
# order = db.query("SELECT * FROM orders WHERE id = ?", req.params.id)
# return order <-- вернёт чужой заказ
# Правильно: владельца берём из сессии и включаем в условие
# order = db.query(
# "SELECT * FROM orders WHERE id = ? AND owner_id = ?",
# req.params.id, session.userId)
# if (!order) return res.status(404) # не палим существование чужого объекта