Files
progcode/editorial/agent-rewrites/256.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
20 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": 256,
"slug": "editorial-2020-11-field-security-baseline",
"title": "CSRF не должен быть обещанием: как проверить защиту административного POST",
"excerpt": "Старый административный POST может принять действие из чужого контекста, даже если в чеклисте написано «CSRF включён». Разбираем механизм, отрицательные проверки и критерий готовности без сканирования живого приложения.",
"contentHtml": "<p>Симптом простой: в release checklist написано «CSRF включён», но никто не может показать запрос, который сервер отклонил из чужого контекста. Старый маршрут <code>POST /admin/profile</code> принимает cookie сессии и обновляет профиль. Форма работает, тест на успешный ответ зелёный, а проверка происхождения запроса отсутствует или живёт только в новом controller. Цена ошибки — не красивый warning. Сайт злоумышленника может заставить браузер администратора отправить нежелательное изменение с его действующей сессией. Ошибка меняет данные до того, как её заметят в журнале.</p>\n<p>Тезис статьи такой: baseline безопасности нужно доказывать не наличием настройки, а независимым отказом на каждом опасном входе. Для одного state-changing маршрута достаточно описать контракт запроса, поставить server-side gate до побочного эффекта, проверить положительный и отрицательные случаи и отдельно подтвердить заголовки ответа. Ниже — учебный пример. Он не проверяет живой сайт, не заменяет аудит и не утверждает production-результат.</p>\n<h2>Механизм: cookie подтверждает сессию, но не намерение</h2>\n<p>Браузер автоматически прикладывает cookie сессии к запросам к знакомому сайту. Сервер видит знакомого пользователя, но из одной cookie не узнаёт, сам ли пользователь нажал кнопку. Запрос мог начаться на другом сайте. CSRF-токен добавляет второй сигнал: форма или клиент получают значение через доверенный контекст, а сервер сравнивает его с ожидаемым значением до изменения состояния.</p>\n<p>Проверка должна находиться на сервере и идти до вызова, который пишет в базу, отправляет платёж, меняет роль или запускает другой необратимый эффект. Проверка в JavaScript не заменяет серверную: скрипт можно не выполнить, изменить или обойти. <code>SameSite</code> у cookie снижает часть cross-site запросов, но не превращает маршрут в универсально защищённый контракт. Для критичного действия нужны явные условия метода, сессии, CSRF-токена и полномочия.</p>\n<pre><code>async function updateProfile(request, response) {\n if (request.method !== 'POST') {\n return response.status(405).end();\n }\n\n const session = await readSession(request);\n if (!session) {\n return response.status(401).end();\n }\n\n if (!sameOrigin(request) || !validCsrfToken(request, session)) {\n return response.status(403).end();\n }\n\n if (!session.permissions.includes('profile:update')) {\n return response.status(403).end();\n }\n\n await profiles.update(session.userId, request.body);\n return response.status(204).end();\n}</code></pre>\n<p>Это учебный код, а не готовая библиотека. Функция <code>validCsrfToken</code> должна сравнивать значения безопасным способом и иметь понятную область действия. <code>sameOrigin</code> не должен доверять произвольному заголовку от клиента без политики приложения. В реальном проекте нужно учесть reverse proxy, смену схемы, несколько доменов и способ доставки формы. Сначала зафиксируйте эти условия. Потом пишите проверку.</p>\n<h2>Что именно нужно доказать</h2>\n<p>У маршрута есть актив: профиль администратора. Есть субъект: пользователь с сессией. Есть действие: изменение профиля. Есть вход: method, path, cookie, origin, token и permission. Есть эффект: запись в хранилище. Такая карта ограничивает проверку. Мы не заявляем, что проверили всё приложение. Мы заявляем только то, что четыре конкретных входа дают ожидаемые решения, а побочный эффект вызывается после gates.</p>\n<p>Положительный случай нужен, чтобы не принять полный запрет за защиту. Сервер должен разрешить запрос с правильным method, доверенным origin, действующей сессией, совпадающим token и правом <code>profile:update</code>. Три отрицательных случая меняют по одному условию. Чужой origin должен получить <code>403</code>. Пустой или неверный token должен получить <code>403</code>. Сессия без write permission тоже должна получить <code>403</code>. Если менять сразу несколько полей, причина решения потеряется.</p>\n<figure><img src=\"/assets/editorial/2020/security-baseline-diagnosis-2020.svg\" alt=\"Схема проверки security baseline: симптом, причина, проверка и действие для одного административного маршрута\"><figcaption>Учебная схема связывает каждый симптом с узкой проверкой и следующим действием. Asset показывает порядок рассуждения, а не результат проверки живого сервера.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Учебная матрица для <code>POST /admin/profile</code></caption><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Чужой origin получает успешный ответ</td><td>Origin не входит в gate или проверяется после update</td><td>Подать только <code>Origin: https://other.example.test</code> и ожидать <code>403</code></td><td>Поставить проверку до побочного эффекта и перечислить допустимые origin</td></tr><tr><td>Пустой token проходит</td><td>Поле потерялось между формой и controller или проверка стала необязательной</td><td>Передать пустой <code>csrfToken</code> при прочих правильных полях</td><td>Требовать совпадение с серверным значением; сам token не писать в лог</td></tr><tr><td>Read-only пользователь меняет профиль</td><td>UI-роль приняли за server-side permission</td><td>Оставить сессию действующей, заменить permission на <code>profile:read</code></td><td>Проверять <code>profile:update</code> перед update и покрыть соседние write-маршруты</td></tr><tr><td>Ответ не содержит ожидаемой policy</td><td>Заголовок добавляется только для главной страницы или теряется на proxy</td><td>Проверить один разрешённый ответ на стенде и сравнить поля contract</td><td>Закрепить область заголовка, проверить версию сервера и путь доставки</td></tr></tbody></table>\n<p>Матрица не назначает severity и не выдаёт сертификат соответствия. Она связывает наблюдение с одной гипотезой. Если foreign origin получил <code>403</code> в локальном unit-тесте, это доказывает только решение этой функции на её входе. Оно не доказывает, что reverse proxy пропустит тот же заголовок, что браузер отправит его так же или что другой маршрут не обновляет тот же объект.</p>\n<h2>Контракт запроса и ответа</h2>\n<p>Сначала запишите безопасный учебный trace. Не используйте реальные cookie, токены, адреса пользователей и идентификаторы записей. Пример ниже показывает форму данных и порядок ответа. Он не является командой для запуска против чужого или production-сервера.</p>\n<pre><code>POST /admin/profile HTTP/1.1\nHost: admin.example.test\nOrigin: https://admin.example.test\nCookie: sid=TRAINING_SESSION\nContent-Type: application/x-www-form-urlencoded\n\ncsrfToken=TRAINING_TOKEN&amp;displayName=Example</code></pre>\n<pre><code>HTTP/1.1 204 No Content\nContent-Security-Policy: default-src 'self'; frame-ancestors 'none'\nSet-Cookie: sid=TRAINING_SESSION; Path=/; Secure; HttpOnly; SameSite=Lax</code></pre>\n<p>В этом trace значения синтетические. Заголовок <code>Origin</code> помогает определить контекст запроса, но не заменяет token и permission. <code>HttpOnly</code> ограничивает доступ к cookie из JavaScript, <code>Secure</code> требует защищённого транспорта, а <code>SameSite</code> задаёт правила отправки cookie. Эти атрибуты уменьшают поверхность атаки, но не исправляют ошибку авторизации в controller. CSP ограничивает ресурсы, которые браузер может загрузить, и помогает с отдельными классами атак. Она не заставляет сервер отличать намеренный POST от подделанного.</p>\n<p>Не смешивайте response contract с доказательством доставки. Если header есть в fixture, он есть в fixture. Чтобы говорить о стенде, нужно получить один фактический ответ через разрешённый канал, учесть proxy и записать версию компонентов. Без этого формулировка должна быть «ожидаемая policy», а не «policy включена».</p>\n<h2>Порядок действий</h2>\n<ol><li>Назовите один актив, один state-changing маршрут и один эффект. Формулировка «проверить весь сайт» слишком широка для первого baseline.</li><li>Опишите положительный request contract: method, path, origin, наличие сессии, CSRF-токен и требуемое permission. Не включайте секреты.</li><li>Проверьте, что каждый gate выполняется на сервере до записи или другого побочного эффекта. Зафиксируйте порядок в коде или тесте.</li><li>Сделайте один разрешённый synthetic request и минимум три отрицательных. В каждом отрицательном случае меняйте только одно условие.</li><li>Проверьте status, отсутствие вызова update для отказанных случаев и безопасный объём логирования. Токены и cookie в логи не попадают.</li><li>Сверьте фактический ответ разрешённого стенда с response contract: <code>Set-Cookie</code>, CSP и область применения заголовков. Не переносите вывод на другие маршруты без отдельной проверки.</li><li>Если отрицательный случай разрешён или побочный эффект вызывается до проверки, остановите выпуск этого маршрута. Сначала верните явный gate, затем повторите весь набор.</li></ol>\n<h2>Как разбирать отрицательный путь</h2>\n<p>Если чужой origin прошёл, не добавляйте CSP в надежде закрыть проблему. CSP управляет поведением браузера, а решение о записи принимает сервер. Найдите место, где controller вызывает update, и поставьте проверку до него. Затем добавьте тест, который меняет только origin. Если тест проходит, он фиксирует именно этот регресс.</p>\n<p>Если пустой token прошёл, проверьте связку формы и controller. Частая ошибка — условие вида «проверить token, если поле присутствует». Для state-changing действия отсутствие поля должно быть отказом. Не исправляйте симптом, отключая проверку для старого клиента. Сначала выясните контракт клиента и выберите явную совместимую миграцию.</p>\n<p>Если read-only пользователь прошёл, ищите владельца полномочия на сервере. Скрытая кнопка в интерфейсе не является authorization. Permission должен проверяться для конкретного действия и конкретного ресурса. Нужен также отрицательный тест для соседнего пользователя, чтобы случайная подмена идентификатора не открыла чужой профиль.</p>\n<p>Если ожидаемый header пропал, отделите генерацию ответа от доставки. Проверьте route, middleware, reverse proxy и кэш. Не называйте policy действующей, пока разрешённый стенд не вернул её фактически. Если header добавляет proxy, проверьте, что внутренний ответ не обходится другим путём.</p>\n<h2>Ограничения учебной проверки</h2>\n<p>Этот материал не сканирует приложение и не проверяет CVE. Он не оценивает TLS, пароли, загрузку файлов, CORS, SQL-инъекции, clickjacking во всех браузерах, гонки, восстановление после ошибки или другие endpoints. Он не подтверждает соответствие OWASP ASVS и не даёт production-результат. Synthetic request показывает форму рассуждения и границу теста. Для реального вывода нужны владелец системы, разрешённый стенд, фактический HTTP-ответ, журналы без секретов и отдельный охват остальных маршрутов.</p>\n<p>Есть и технические ограничения. Сравнение origin зависит от схемы, host и proxy-конфигурации. Политика cookie зависит от домена, пути, TLS и сценария авторизации. CSRF-токен должен иметь жизненный цикл, привязанный к выбранной модели сессии. Нельзя копировать код из примера без проверки фреймворка, версии сервера и способа маршрутизации. Учебный фрагмент ограничен намеренно: он показывает механизм, но не скрывает места, где проект должен принять собственное решение.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Baseline для этого маршрута готов, когда положительный synthetic request проходит, каждый отрицательный случай получает ожидаемый отказ, а <code>update</code> не вызывается ни для одного отказа. На разрешённом стенде фактический ответ совпадает с зафиксированным response contract. В записи проверки указаны маршрут, граница вывода, версия компонентов и оставшиеся непроверенные пути. Если хотя бы один из этих пунктов отсутствует, результат нужно назвать частичным.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">OWASP: Cross-Site Request Forgery Prevention Cheat Sheet</a> — механизм CSRF и требования к server-side защите.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie\" target=\"_blank\" rel=\"noopener\">MDN: Set-Cookie</a> — атрибуты <code>Secure</code>, <code>HttpOnly</code> и <code>SameSite</code>.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP\" target=\"_blank\" rel=\"noopener\">MDN: Content Security Policy</a> — область действия CSP и её место среди защитных механизмов.</li></ul>"
}