Руководства по безопасности
Добавили вход и решили, что защищено? — аутентификация и авторизация
Аутентификация (кто) и авторизация (что можно делать) — разное. Вход без привязки данных к владельцу даёт «вошёл = видит все данные». Почему так и как защититься.
«Я добавил вход, значит теперь защищено» — это опасное допущение. Вход (аутентификация) и права (авторизация) — разные вещи, и путаница между ними приводит к инциденту, когда все, кто вошёл, видят все данные, включая чужие. Никаких шагов атаки — только как это происходит и как защититься.
Аутентификация и авторизация — разные вещи
Их легко спутать, но роли у них совершенно разные.
Только аутентификация (частая ошибка)
- считать «может войти = безопасно»
- обрабатывать чтение данных после входа одинаково для всех
- итог: все, кто вошёл, добираются до всех данных
Аутентификация + авторизация (как должно быть)
- аутентификация подтверждает «кто»
- авторизация ограничивает «только теми данными, к которым можно обращаться»
- чтение данных всегда привязано к владельцу
Аутентификация — это «проверка документов на входе»; авторизация — «контроль того, в какие комнаты можно войти». Вход гарантирует только первое. «Кто к каким данным может обращаться» нужно писать отдельно.
Как «видит все данные» возникает в реальных приложениях
На практике одна и та же дыра снова и снова находится в независимых самописных системах. Личное приложение для задач/заметок или небольшая система управления товарами/заказами (с интеграциями внешних API) — причина была одна и та же: наложение двух изъянов.
① Регистрация открыта (и на двух маршрутах)
Auth-scaffold (вроде Laravel Breeze/Jetstream) часто по умолчанию поставляются с публичной страницей регистрации. А когда маршрут регистрации дублируется в UI-библиотеке и в auth-пакете, закрытие одного всё равно оставляет /register. Посторонний, знающий URL, может зарегистрироваться.
② Аутентификация есть, а авторизации нет (нет привязки к владельцу)
В таблице данных нет столбца владельца (user_id), либо запросы списка/чтения/обновления/удаления не сужены до текущего пользователя. Тогда «вошёл = доступ ко всем данным». Если приложение автоматически входит сразу после регистрации, посторонний попадает на чужой список данных в момент регистрации.
Итог: инцидент authn ≠ authz
Вход (аутентификация) есть, а ключей от комнат (авторизации) нет. Поскольку посторонний ведёт себя как «нормально вошедший пользователь», от несанкционированного доступа почти не остаётся следов.
Радиус поражения тем больше, чем больше задействовано бизнес-данных (могут пострадать записи клиентов, заказы, ключи интеграционных API). А другая система, построенная так же, скорее всего, имеет ту же дыру.
Как защититься
Всегда привязывайте данные к владельцу
Ограничивайте каждое чтение и запись «собственными строками вошедшего пользователя». Обеспечивайте условие владельца во всех действиях чтения/обновления/удаления через глобальный scope в ORM или политику авторизации (policy/gate). Если в таблице нет столбца владельца, сначала перепроектируйте её.
Закрывайте ненужную регистрацию — и подозревайте дублирование маршрутов
Отключайте регистрацию в личных/внутренних приложениях. Реально измерьте по HTTP, что /register и подобные возвращают 404, и проверьте оба маршрута регистрации — и в UI-библиотеке, и в auth-пакете.
Эшелонированная оборона на административных интерфейсах
Для внутреннего/административного использования не полагайтесь только на аутентификацию — добавляйте IP-ограничения, Basic-аутентификацию и подобное. Если одна стена падает, следующая останавливает.
Аудит и логи доступа *до* инцидента
Без «кто, когда, с какого IP, что сделал» после инцидента нельзя восстановить, «что было увидено». Как минимум ведите аудит успешных/неуспешных входов и храните с ротацией веб-логи доступа.
Отслеживайте новые регистрации и аномалии
Ключевой урок здесь — «долгое время оставалось незамеченным». Добавьте способы замечать быстро — уведомления о регистрации нового пользователя, периодическую инвентаризацию пользователей/данных. Без детектирования дыра тихо остаётся открытой.
Взгляд этого сайта: библиотека гарантирует только «кто»
Auth-библиотека берёт на себя «кто» — и не более. «Что этому человеку можно делать» не существует, пока вы, проектировщик, это не напишете. И — структурный изъян, найденный в одной системе, вероятно, есть и в других системах, построенных так же. Не исправляйте один и не останавливайтесь; проверяйте все однотипные сборки. Когда нельзя определить, было ли что-то прочитано, правильная позиция — считать, что прочитано, и отозвать/сменить любые раскрытые секреты.
Читать далее
- Глоссарий: что такое IDOR (нарушение контроля доступа) (классика: подставьте чужой ID и доберётесь до чужих данных)
- Применение: безопасность по фреймворкам (где писать авторизацию в каждом фреймворке)
- Связанное: не держите секреты в публичных каталогах (проверка утечки token/credentials/.env)
- Основы: защита секретов с нуля / фундамент: минимальный чеклист безопасности
Источники
- OWASP Top 10 (2021) A01: Broken Access Control: owasp.org
- OWASP Authorization Cheat Sheet: cheatsheetseries.owasp.org
FAQ
QЧем аутентификация и авторизация отличаются?
Аутентификация проверяет, кто вы (вход). Авторизация решает, что вам можно делать (права). По аналогии: аутентификация — это проверка документов на входе в здание; авторизация определяет, в какие комнаты вам можно войти. Добавляя вход, вы реализуете только аутентификацию — авторизацию (кто к каким данным может обращаться) нужно писать отдельно. Смешаете эти два понятия — и получите «все, кто вошёл, видят все данные».
QВход есть — почему же все видят все данные?
Потому что запрос к данным не сужен до «только строки вошедшего пользователя». Забудьте условие владельца (user_id) в любом из действий — список/чтение/обновление/удаление — и после входа возвращаются все строки, включая чужие. Это дыра в проектировании, не зависящая от наличия аутентификации, и это классический Broken Access Control, который OWASP ставит на первое место.
QДля личного инструмента или внутреннего приложения ведь можно оставить регистрацию открытой?
Это опасно. Многие auth-scaffold по умолчанию поставляются с публичной регистрацией (например, /register). Если посторонний знает URL, он может зарегистрироваться, и без авторизации сразу попадает к данным внутри. Маршруты регистрации иногда дублируются (и в UI-библиотеке, и в auth-пакете), поэтому, закрыв один, вы не закрываете всё. Отключайте регистрацию, которая вам не нужна, и добавляйте эшелонированную защиту вроде IP-ограничений на административные интерфейсы.