8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 257,
|
||
"slug": "editorial-2020-11-mechanism-security-baseline",
|
||
"title": "Граница запроса: как собрать минимальный security baseline для state change",
|
||
"excerpt": "Один защищаемый POST показывает, почему сессия, CSRF-контекст, permission и response policy решают разные задачи. В статье есть учебный контракт, матрица проверки и критерий готовности без иллюзии production-аудита.",
|
||
"contentHtml": "<p>Симптом выглядит безобидно: старый <code>POST /admin/profile</code> возвращает успешный ответ, если браузер прислал cookie сессии. При этом запрос с чужого origin или ролью только для чтения тоже доходит до изменения display name. В логах есть «пользователь вошёл», а отдельного доказательства права на действие нет. Цена ошибки — не только изменённое поле. Команда принимает identity за authorization, считает наличие headers защитой маршрута и выпускает код с ложным verdict.</p>\n<p>Тезис простой: минимальный security baseline — это не список cookie flags и не один заголовок. Это цепочка независимых решений до побочного эффекта: сервер узнаёт субъекта, проверяет контекст state change, проверяет право на конкретный ресурс и только потом меняет состояние. Ответ добавляет свои ограничения для браузера, но не заменяет проверки запроса.</p>\n<h2>Что именно защищает baseline</h2>\n<p>Возьмём учебную HTML-форму на origin <code>https://admin.example.test</code>. Она меняет имя профиля. Активом здесь является запись профиля, а опасным эффектом — запись нового значения в базу. Нам не нужно сразу описывать весь сайт. Для первой проверки достаточно назвать один маршрут, один актив, одного субъекта и один эффект.</p>\n<p>Сессия отвечает на вопрос «кого сервер узнал». Она не отвечает на вопрос «может ли этот субъект менять профиль». Origin и CSRF-токен связывают запрос с ожидаемым контекстом формы. Они не выдают permission. Permission отвечает на вопрос «разрешено ли этому субъекту именно это действие». CSP и атрибуты cookie ограничивают поведение браузера и передачу состояния. Они не валидируют бизнес-операцию.</p>\n<p>Такое разделение помогает найти отрицательный путь. Если неизвестная сессия получает <code>401</code>, это проверяет только identity gate. Если известная сессия с permission <code>profile:read</code> получает <code>403</code>, срабатывает authorization gate. Если запрос из чужого origin или без токена получает <code>403</code> до вызова update, проверяется контекст формы. Один положительный ответ не доказывает ни один из этих отрицательных сценариев.</p>\n<figure><img src=\"/assets/editorial/2020/security-baseline-request-boundary-2020.svg\" alt=\"Схема учебного POST-маршрута: браузер передаёт запрос через proxy в приложение, приложение проверяет session, origin, CSRF token и permission до state change, а ответ несёт cookie и CSP contract\" loading=\"lazy\" /><figcaption>Границы запроса идут последовательно. Cookie сообщает о состоянии сессии, permission разрешает действие, а CSP остаётся дополнительным ограничением ответа.</figcaption></figure>\n<h2>Механизм: решения до побочного эффекта</h2>\n<p>Удобно представить обработчик как функцию, которая сначала строит решение, а потом вызывает запись. Каждый gate должен вернуть понятный отказ. Нельзя обновлять профиль, а затем выяснять, был ли origin допустимым. Нельзя переносить permission в шаблон или проверять его только в JavaScript: клиент может отправить запрос напрямую.</p>\n<pre><code>async 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}</code></pre>\n<p>Это не готовая библиотека и не рекомендация копировать код без адаптации. Пример показывает порядок. `sessions.find` подтверждает субъекта. Проверка origin и токена защищает контекст формы. `profile:write` проверяет право. `validateDisplayName` отвечает за контракт данных и всё равно не заменяет authorization. `response` формирует учебный контракт ответа. В production нужно использовать проверенные средства управления сессиями и CSRF, единый формат ошибок и атомарное правило: ни один отказ не вызывает update.</p>\n<p>Код также показывает отрицательный путь. При пустом токене обработчик завершится до изменения. При роли <code>profile:read</code> он завершится до изменения. При неизвестной сессии не появится запись в профиле. Учебный пример не запускает сервер, не открывает сеть и не утверждает, что настоящий браузер получил эти headers. Его verdict ограничен моделью входов и порядком решений.</p>\n<h2>Cookie, CSP и permission нельзя смешивать</h2>\n<p>Cookie переносит состояние между запросами. Атрибут <code>Secure</code> ограничивает отправку cookie HTTPS-сценарием, <code>HttpOnly</code> запрещает доступ к нему из обычного JavaScript, а <code>SameSite</code> задаёт правила отправки в cross-site-контексте. Эти свойства снижают риск, но не создают право <code>profile:write</code>. Сервер должен извлечь субъект из сессии и отдельно проверить разрешение.</p>\n<p>CSP ограничивает ресурсы, которые страница может загружать или исполнять. Политика с <code>frame-ancestors 'none'</code> помогает запретить встраивание страницы в frame. Она остаётся defense in depth. CSP не исправляет небезопасный SQL, не проверяет роль и не останавливает запрос, который уже прошёл application gate. Если header добавляет proxy, проверяйте его на реальном маршруте и на ошибочных ответах отдельно. Наличие строки в конфигурации не доказывает доставку header.</p>\n<p>Permission тоже имеет границу. Роль должна проверяться на сервере рядом с операцией, которая меняет актив. Общая проверка «пользователь администратор» часто слишком широка: разные административные функции имеют разные владельцы и последствия. Назовите минимальное разрешение. Для этой формы это <code>profile:write</code>, а не абстрактное <code>admin</code>.</p>\n<h2>Матрица диагностики</h2>\n<div class=\"table-scroll\"><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Вероятная причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Запрос с чужого origin меняет профиль</td><td>Нет проверки контекста формы или она стоит после update</td><td>Отправить тот же учебный input с другим origin и проверить отсутствие вызова update</td><td>Поставить server-side origin/CSRF gate до побочного эффекта</td></tr><tr><td>Роль read-only получает успешный ответ</td><td>Сессия считается достаточной авторизацией</td><td>Оставить session валидной и заменить permission на <code>profile:read</code></td><td>Проверять точное право <code>profile:write</code> рядом с операцией</td></tr><tr><td>Cookie есть, но поведение сессии различается</td><td>Неполный или слишком широкий cookie contract</td><td>Проверить <code>Set-Cookie</code>, HTTPS, область Path и cross-site-сценарий</td><td>Сузить атрибуты и подтвердить их на фактическом ответе сервера</td></tr><tr><td>CSP записан в конфигурации, но страница грузит лишний ресурс</td><td>Header не дошёл, policy шире нужного или маршрут обходит proxy</td><td>Посмотреть response headers на странице и на ошибке, затем сопоставить policy с assets</td><td>Исправить точку выдачи или policy; не считать конфигурационный diff доказательством</td></tr><tr><td>Отказ виден клиенту, но запись уже появилась</td><td>Проверка выполняется после изменения или ошибка скрывает частичный успех</td><td>Сравнить audit/event и состояние профиля после каждого отрицательного input</td><td>Сделать все gates предварительными и проверить транзакционную границу</td></tr></tbody></table></div>\n<p>Матрица полезна тем, что связывает наблюдаемый симптом с одним экспериментом. Не меняйте одновременно cookie, middleware и permission. Если изменились три границы, следующий результат не показывает, что именно исправило проблему. Для каждого отрицательного случая меняйте одно условие и фиксируйте ожидаемый статус, отсутствие побочного эффекта и причину отказа.</p>\n<h2>Порядок первой проверки</h2>\n<ol><li>Назовите маршрут, актив и операцию. Запишите, какое состояние изменится и кто владеет этим состоянием.</li><li>Опишите минимальный request contract без секретов: метод, path, учебный origin, факт сессии, CSRF-токен и permission.</li><li>Сделайте один положительный случай и минимум три отрицательных: неизвестная сессия, чужой origin или пустой токен, валидная сессия с read-only permission.</li><li>Проверьте, что каждый отрицательный случай останавливается до вызова update. Статус ответа сам по себе недостаточен: состояние должно остаться прежним.</li><li>Проверьте response contract на успехе и отказе: cookie attributes, CSP, формат ошибки и отсутствие утечки лишних данных. Отдельно подтвердите фактические headers на разрешённом стенде.</li><li>Запишите границы verdict. Укажите, что проверено в модели, а что требует отдельной проверки browser, proxy, TLS, API-клиентов и других маршрутов.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Этот baseline не является полным аудитом приложения. Он не проверяет хранение паролей, восстановление аккаунта, загрузку файлов, SQL injection, XSS в каждом выводе, CORS, rate limiting, управление секретами, зависимости, TLS, логи, резервное копирование и внешний сетевой периметр. Он также не доказывает, что CSP совместима со всеми клиентами, а cookie действительно доставляется через каждый proxy.</p>\n<p>Нельзя расширять verdict учебного теста фразой «маршрут защищён». Корректнее сказать: «в заданной модели запрос с неизвестной сессией, неверным контекстом и недостаточным permission не достигает state change; response contract содержит проверяемые поля». Для production нужны интеграционные проверки, реальные браузеры, наблюдение за отказами и отдельная модель угроз для каждого нового актива.</p>\n<p>Отрицательный путь важнее красивого положительного примера. Если update вызывается хотя бы при одном неверном входе, baseline не готов. Если header присутствует только на <code>200</code>, а на <code>4xx</code> исчезает, это отдельный дефект response policy. Если permission проверяется в UI, а не в обработчике, контроль отсутствует там, где его может обойти клиент.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Маршрут можно считать прошедшим этот baseline, когда для одного явно названного state change сохранены положительный и отрицательные сценарии, каждый gate выполняется до побочного эффекта, а проверка подтверждает неизменность актива после отказа. На разрешённом стенде отдельно видны фактические cookie и CSP headers. В отчёте есть ссылка на маршрут, ожидаемые решения, полученные статусы, границы учебной модели и список непроверенных угроз. Если хотя бы одно из этих условий отсутствует, результат — не «безопасно», а «нужно продолжить проверку».</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://owasp.org/www-project-application-security-verification-standard/\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP Application Security Verification Standard</a> — официальный каталог проверяемых требований к web application security; конкретные проверки нужно сопоставлять с моделью угроз приложения.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc6265\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 6265: HTTP State Management Mechanism</a> — спецификация взаимодействия <code>Set-Cookie</code> и <code>Cookie</code>; она описывает транспорт состояния, а не бизнес-авторизацию.</li><li><a href=\"https://www.w3.org/TR/CSP3/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Content Security Policy Level 3</a> — официальная спецификация CSP и её роли как дополнительного ограничения ресурсов; статус документа нужно учитывать при выборе поддерживаемых возможностей.</li></ul>"
|
||
}
|