Files
progcode/editorial/agent-rewrites/257.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
19 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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>"
}