{ "index": 257, "slug": "editorial-2020-11-mechanism-security-baseline", "title": "Граница запроса: как собрать минимальный security baseline для state change", "excerpt": "Один защищаемый POST показывает, почему сессия, CSRF-контекст, permission и response policy решают разные задачи. В статье есть учебный контракт, матрица проверки и критерий готовности без иллюзии production-аудита.", "contentHtml": "
Симптом выглядит безобидно: старый POST /admin/profile возвращает успешный ответ, если браузер прислал cookie сессии. При этом запрос с чужого origin или ролью только для чтения тоже доходит до изменения display name. В логах есть «пользователь вошёл», а отдельного доказательства права на действие нет. Цена ошибки — не только изменённое поле. Команда принимает identity за authorization, считает наличие headers защитой маршрута и выпускает код с ложным verdict.
Тезис простой: минимальный security baseline — это не список cookie flags и не один заголовок. Это цепочка независимых решений до побочного эффекта: сервер узнаёт субъекта, проверяет контекст state change, проверяет право на конкретный ресурс и только потом меняет состояние. Ответ добавляет свои ограничения для браузера, но не заменяет проверки запроса.
\nВозьмём учебную HTML-форму на origin https://admin.example.test. Она меняет имя профиля. Активом здесь является запись профиля, а опасным эффектом — запись нового значения в базу. Нам не нужно сразу описывать весь сайт. Для первой проверки достаточно назвать один маршрут, один актив, одного субъекта и один эффект.
Сессия отвечает на вопрос «кого сервер узнал». Она не отвечает на вопрос «может ли этот субъект менять профиль». Origin и CSRF-токен связывают запрос с ожидаемым контекстом формы. Они не выдают permission. Permission отвечает на вопрос «разрешено ли этому субъекту именно это действие». CSP и атрибуты cookie ограничивают поведение браузера и передачу состояния. Они не валидируют бизнес-операцию.
\nТакое разделение помогает найти отрицательный путь. Если неизвестная сессия получает 401, это проверяет только identity gate. Если известная сессия с permission profile:read получает 403, срабатывает authorization gate. Если запрос из чужого origin или без токена получает 403 до вызова update, проверяется контекст формы. Один положительный ответ не доказывает ни один из этих отрицательных сценариев.
Удобно представить обработчик как функцию, которая сначала строит решение, а потом вызывает запись. Каждый gate должен вернуть понятный отказ. Нельзя обновлять профиль, а затем выяснять, был ли origin допустимым. Нельзя переносить permission в шаблон или проверять его только в JavaScript: клиент может отправить запрос напрямую.
\nasync function updateProfile(request) {\n const session = await sessions.find(request.cookies.sid);\n if (!session) return response(401);\n\n if (request.origin !== 'https://admin.example.test') {\n return response(403);\n }\n\n if (!csrf.verify(request.body.csrf, session.csrfSecret)) {\n return response(403);\n }\n\n if (!session.permissions.includes('profile:write')) {\n return response(403);\n }\n\n await profiles.update(session.userId, {\n displayName: validateDisplayName(request.body.displayName),\n });\n\n return response(204, {\n 'Content-Security-Policy': \"default-src 'self'; frame-ancestors 'none'\",\n 'Set-Cookie': 'sid=...; Path=/; Secure; HttpOnly; SameSite=Lax',\n });\n}\nЭто не готовая библиотека и не рекомендация копировать код без адаптации. Пример показывает порядок. `sessions.find` подтверждает субъекта. Проверка origin и токена защищает контекст формы. `profile:write` проверяет право. `validateDisplayName` отвечает за контракт данных и всё равно не заменяет authorization. `response` формирует учебный контракт ответа. В production нужно использовать проверенные средства управления сессиями и CSRF, единый формат ошибок и атомарное правило: ни один отказ не вызывает update.
\nКод также показывает отрицательный путь. При пустом токене обработчик завершится до изменения. При роли profile:read он завершится до изменения. При неизвестной сессии не появится запись в профиле. Учебный пример не запускает сервер, не открывает сеть и не утверждает, что настоящий браузер получил эти headers. Его verdict ограничен моделью входов и порядком решений.
Cookie переносит состояние между запросами. Атрибут Secure ограничивает отправку cookie HTTPS-сценарием, HttpOnly запрещает доступ к нему из обычного JavaScript, а SameSite задаёт правила отправки в cross-site-контексте. Эти свойства снижают риск, но не создают право profile:write. Сервер должен извлечь субъект из сессии и отдельно проверить разрешение.
CSP ограничивает ресурсы, которые страница может загружать или исполнять. Политика с frame-ancestors 'none' помогает запретить встраивание страницы в frame. Она остаётся defense in depth. CSP не исправляет небезопасный SQL, не проверяет роль и не останавливает запрос, который уже прошёл application gate. Если header добавляет proxy, проверяйте его на реальном маршруте и на ошибочных ответах отдельно. Наличие строки в конфигурации не доказывает доставку header.
Permission тоже имеет границу. Роль должна проверяться на сервере рядом с операцией, которая меняет актив. Общая проверка «пользователь администратор» часто слишком широка: разные административные функции имеют разные владельцы и последствия. Назовите минимальное разрешение. Для этой формы это profile:write, а не абстрактное admin.
| Симптом | Вероятная причина | Проверка | Действие |
|---|---|---|---|
| Запрос с чужого origin меняет профиль | Нет проверки контекста формы или она стоит после update | Отправить тот же учебный input с другим origin и проверить отсутствие вызова update | Поставить server-side origin/CSRF gate до побочного эффекта |
| Роль read-only получает успешный ответ | Сессия считается достаточной авторизацией | Оставить session валидной и заменить permission на profile:read | Проверять точное право profile:write рядом с операцией |
| Cookie есть, но поведение сессии различается | Неполный или слишком широкий cookie contract | Проверить Set-Cookie, HTTPS, область Path и cross-site-сценарий | Сузить атрибуты и подтвердить их на фактическом ответе сервера |
| CSP записан в конфигурации, но страница грузит лишний ресурс | Header не дошёл, policy шире нужного или маршрут обходит proxy | Посмотреть response headers на странице и на ошибке, затем сопоставить policy с assets | Исправить точку выдачи или policy; не считать конфигурационный diff доказательством |
| Отказ виден клиенту, но запись уже появилась | Проверка выполняется после изменения или ошибка скрывает частичный успех | Сравнить audit/event и состояние профиля после каждого отрицательного input | Сделать все gates предварительными и проверить транзакционную границу |
Матрица полезна тем, что связывает наблюдаемый симптом с одним экспериментом. Не меняйте одновременно cookie, middleware и permission. Если изменились три границы, следующий результат не показывает, что именно исправило проблему. Для каждого отрицательного случая меняйте одно условие и фиксируйте ожидаемый статус, отсутствие побочного эффекта и причину отказа.
\nЭтот baseline не является полным аудитом приложения. Он не проверяет хранение паролей, восстановление аккаунта, загрузку файлов, SQL injection, XSS в каждом выводе, CORS, rate limiting, управление секретами, зависимости, TLS, логи, резервное копирование и внешний сетевой периметр. Он также не доказывает, что CSP совместима со всеми клиентами, а cookie действительно доставляется через каждый proxy.
\nНельзя расширять verdict учебного теста фразой «маршрут защищён». Корректнее сказать: «в заданной модели запрос с неизвестной сессией, неверным контекстом и недостаточным permission не достигает state change; response contract содержит проверяемые поля». Для production нужны интеграционные проверки, реальные браузеры, наблюдение за отказами и отдельная модель угроз для каждого нового актива.
\nОтрицательный путь важнее красивого положительного примера. Если update вызывается хотя бы при одном неверном входе, baseline не готов. Если header присутствует только на 200, а на 4xx исчезает, это отдельный дефект response policy. Если permission проверяется в UI, а не в обработчике, контроль отсутствует там, где его может обойти клиент.
Маршрут можно считать прошедшим этот baseline, когда для одного явно названного state change сохранены положительный и отрицательные сценарии, каждый gate выполняется до побочного эффекта, а проверка подтверждает неизменность актива после отказа. На разрешённом стенде отдельно видны фактические cookie и CSP headers. В отчёте есть ссылка на маршрут, ожидаемые решения, полученные статусы, границы учебной модели и список непроверенных угроз. Если хотя бы одно из этих условий отсутствует, результат — не «безопасно», а «нужно продолжить проверку».
\nSet-Cookie и Cookie; она описывает транспорт состояния, а не бизнес-авторизацию.