Что должно быть атомарным в операции, если между проверкой права и изменением ресурса его состояние может измениться?
Атомарными должны быть проверка условий авторизации и изменение ресурса, чтобы злоумышленник не успел изменить состояние между этими шагами. Иначе возникает уязвимость TOCTOU: система проверяет одно состояние объекта, а использует уже другое.
Проблема разделения проверки и использования появилась в системах с конкурентным доступом к общим ресурсам: файловой системе, базах данных и многопроцессных приложениях. Даже корректная проверка становится недостаточной, если состояние объекта может измениться до фактического действия.
Изначально такие ошибки рассматривались как разновидность гонок. В архитектуре безопасности они важны потому, что граница доверия проходит не только между пользователем и сервисом, но и между отдельными шагами одной операции.
Предположим, сервис сначала проверяет, что пользователь имеет право изменить документ, а затем отдельным запросом меняет его статус. Между запросами другой процесс может сменить владельца документа, его состояние или связанные ограничения.
Если сервис не проверит актуальное состояние повторно или не свяжет проверку с изменением атомарно, операция будет выполнена на основании устаревшего решения. Последствия включают обход разграничения доступа, изменение уже заблокированного объекта или нарушение бизнес-ограничений.
Надёжный вариант — выполнить проверку права и переход состояния в одной атомарной операции. На практике для этого используют транзакцию с подходящей изоляцией, блокировку изменяемой записи либо условное изменение с проверкой версии объекта.
Важно проверять не только роль пользователя, но и актуальные атрибуты ресурса: владельца, статус, принадлежность организации, лимиты и версию. Если условие больше не выполняется, операция должна завершиться отказом, а не продолжиться по результату старой проверки.
Оптимистическая конкуренция обычно не блокирует ресурс надолго: операция записывается только при совпадении ожидаемой версии. При конфликте сервис сообщает об ошибке или повторяет операцию с новым состоянием. Пессимистическая блокировка проще для строгой последовательности, но может снизить параллелизм и привести к ожиданиям или взаимным блокировкам.
Транзакция базы данных сама по себе не защищает внешний побочный эффект. Если после изменения записи нужно отправить платёж или сообщение, применяют отдельные гарантии: идемпотентность, паттерн outbox или согласованный протокол с внешней системой.
Нельзя полагаться только на повторную проверку в интерфейсе или на предварительную проверку в API-шлюзе. Защита должна находиться рядом с операцией, которая фактически изменяет ресурс, поскольку только там можно согласовать проверку с актуальным состоянием.
Сервис согласования заявок проверяет, что сотрудник является текущим руководителем подразделения, после чего переводит заявку из состояния на согласовании в одобренное. Пока выполнялись два отдельных шага, заявку можно было перепривязать к другому подразделению.
Рассматривались три варианта. Повторная проверка перед записью уменьшала окно гонки, но не устраняла его; длительная блокировка заявки защищала последовательность, но ухудшала параллельную обработку; условное обновление с проверкой владельца, статуса и версии сохраняло производительность, но требовало корректной обработки конфликтов.
Выбрали транзакцию с условным изменением записи: переход выполнялся только при совпадении всех проверяемых атрибутов и версии. При изменении заявки операция отклонялась, а клиент получал необходимость заново загрузить состояние. Это устранило обход проверки без длительных блокировок.
Нет, если между повторной проверкой и записью всё ещё возможна гонка. Проверка должна быть частью той же атомарной операции, что и изменение, либо запись должна содержать условие, которое проверяется самой операцией изменения.
Нет. При оптимистическом подходе можно использовать версию или другой неизменяемый маркер состояния и принимать изменение только при его совпадении. Блокировка предпочтительна, когда конфликтов много или бизнес-операция требует строго последовательного доступа, но она дороже по задержкам и ресурсам.
Не полностью. Транзакция защищает согласованность локальных данных, но внешний вызов может выполниться дважды, не выполниться после фиксации транзакции или завершиться независимо от отката. Для такой границы нужны идемпотентный идентификатор операции, надёжная публикация события через outbox и обработка повторов.