{ "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.

\n

Тезис простой: минимальный security baseline — это минимальный набор независимых проверок, а не список cookie flags и не один заголовок. Для state change (изменения состояния) сервер сначала узнаёт субъекта, затем проверяет источник запроса и CSRF-контекст, после этого — право на конкретный ресурс, и только потом меняет состояние. Ответ добавляет ограничения для браузера, но не заменяет проверки запроса.

\n

Что именно защищает baseline

\n

Возьмём учебную HTML-форму на origin https://admin.example.test. Она меняет имя профиля. Активом здесь является запись профиля, а опасным эффектом — запись нового значения в базу. Нам не нужно сразу описывать весь сайт. Для первой проверки достаточно назвать один маршрут, один актив, одного субъекта и один эффект.

\n

Сессия отвечает на вопрос «кого сервер узнал». Она не отвечает на вопрос «может ли этот субъект менять профиль». Origin показывает, откуда пришёл запрос, а CSRF-токен связывает его с ожидаемым контекстом формы; ни один из них не выдаёт permission. Permission отвечает на вопрос «разрешено ли этому субъекту именно это действие». CSP и атрибуты cookie ограничивают поведение браузера и передачу состояния. Они не валидируют бизнес-операцию.

\n

Такое разделение помогает найти отрицательный путь. Если неизвестная сессия получает 401, это проверяет только identity gate. Если известная сессия с permission profile:read получает 403, срабатывает authorization gate. Если запрос из чужого origin или без токена получает 403 до вызова update, проверяется контекст формы. Один положительный ответ не доказывает ни один из этих отрицательных сценариев.

\n
\"Схема
Границы запроса идут последовательно. Cookie сообщает о состоянии сессии, permission разрешает действие, а CSP остаётся дополнительным ограничением ответа.
\n

Механизм: решения до побочного эффекта

\n

Удобно представить обработчик как функцию, которая сначала строит решение, а потом вызывает запись. Каждый gate должен вернуть понятный отказ. Нельзя обновлять профиль, а затем выяснять, был ли origin допустимым. Нельзя переносить permission в шаблон или проверять его только в JavaScript: клиент может отправить запрос напрямую.

\n
async function updateProfile(request) {\n  const session = await sessions.find(request.cookies.sid);\n  if (!session) {\n    return response(401, {\n      'WWW-Authenticate': 'Session realm=\"admin\"',\n    });\n  }\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    'Set-Cookie': 'sid=...; Path=/; Secure; HttpOnly; SameSite=Lax',\n  });\n}
\n

Это не готовая библиотека и не рекомендация копировать код без адаптации. Пример показывает порядок. `sessions.find` подтверждает субъекта. Проверка origin и токена защищает контекст формы. `profile:write` проверяет право. `validateDisplayName` отвечает за контракт данных и всё равно не заменяет authorization. `response` формирует учебный контракт ответа. Для `401` добавлен `WWW-Authenticate`, потому что этого требует HTTP-контракт; схема `Session` здесь условная и в реальном приложении должна соответствовать его механизму входа. CSP не включён в ответ `204`: его нужно проверять на HTML-ответе страницы, которую он ограничивает. В production нужны проверенные средства управления сессиями и CSRF, единый формат ошибок и атомарное правило: ни один отказ не вызывает update.

\n

Код также показывает отрицательный путь. При пустом токене обработчик завершится до изменения. При роли profile:read он завершится до изменения. При неизвестной сессии не появится запись в профиле. Учебный пример не запускает сервер, не открывает сеть и не утверждает, что настоящий браузер получил эти headers. Его verdict ограничен моделью входов и порядком решений.

\n

Cookie, CSP и permission нельзя смешивать

\n

Cookie переносит состояние между запросами. Атрибут Secure ограничивает отправку cookie HTTPS-сценарием, HttpOnly запрещает доступ к нему из обычного JavaScript, а SameSite задаёт правила отправки в cross-site-контексте. Эти свойства снижают риск, но не создают право profile:write. Сервер должен извлечь субъект из сессии и отдельно проверить разрешение.

\n

CSP ограничивает ресурсы, которые страница может загружать или исполнять. Политика с frame-ancestors 'none' помогает запретить встраивание страницы в frame. Она остаётся defense in depth и относится к HTML-ответу страницы, а не автоматически к API-ответу 204. CSP не исправляет небезопасный SQL, не проверяет роль и не останавливает запрос, который уже прошёл application gate. Если header добавляет proxy, проверяйте его на реальном маршруте и на ошибочных ответах отдельно. Наличие строки в конфигурации не доказывает доставку header.

\n

Permission тоже имеет границу. Роль должна проверяться на сервере рядом с операцией, которая меняет актив. Общая проверка «пользователь администратор» часто слишком широка: разные административные функции имеют разные владельцы и последствия. Назовите минимальное разрешение. Для этой формы это profile:write, а не абстрактное admin.

\n

Матрица диагностики

\n
Симптом → причина → проверка → действие
СимптомВероятная причинаПроверкаДействие
Запрос с чужого 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 предварительными и проверить транзакционную границу
\n

Матрица полезна тем, что связывает наблюдаемый симптом с одним экспериментом. Не меняйте одновременно cookie, middleware и permission. Если изменились три границы, следующий результат не показывает, что именно исправило проблему. Для каждого отрицательного случая меняйте одно условие и фиксируйте ожидаемый статус, отсутствие побочного эффекта и причину отказа.

\n

Порядок первой проверки

\n
  1. Назовите маршрут, актив и операцию. Запишите, какое состояние изменится и кто владеет этим состоянием.
  2. Опишите минимальный request contract без секретов: метод, path, учебный origin, факт сессии, CSRF-токен и permission.
  3. Сделайте один положительный случай и минимум три отрицательных: неизвестная сессия, чужой origin или пустой токен, валидная сессия с read-only permission.
  4. Проверьте, что каждый отрицательный случай останавливается до вызова update. Статус ответа сам по себе недостаточен: состояние должно остаться прежним.
  5. Проверьте response contract на успехе и отказе: cookie attributes, CSP, формат ошибки и отсутствие утечки лишних данных. Отдельно подтвердите фактические headers на разрешённом стенде.
  6. Запишите границы verdict. Укажите, что проверено в модели, а что требует отдельной проверки browser, proxy, TLS, API-клиентов и других маршрутов.
\n

Ограничения и отрицательный путь

\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, а не в обработчике, контроль отсутствует там, где его может обойти клиент.

\n

Проверяемый критерий готовности

\n

Маршрут можно считать прошедшим этот baseline, когда для одного явно названного state change сохранены положительный и отрицательные сценарии, каждый gate выполняется до побочного эффекта, а проверка подтверждает неизменность актива после отказа. На разрешённом стенде отдельно видны фактические cookie и CSP headers. В отчёте есть ссылка на маршрут, ожидаемые решения, полученные статусы, границы учебной модели и список непроверенных угроз. Если хотя бы одно из этих условий отсутствует, результат — не «безопасно», а «нужно продолжить проверку».

\n

Проверяемые источники

" }