Files
progcode/editorial/agent-rewrites/258.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
17 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": 258,
"slug": "editorial-2020-11-practice-security-baseline",
"title": "Старая админка без CSRF: как проверить минимальный контур защиты",
"excerpt": "Один state-changing маршрут показывает, где заканчивается сессия, начинаются CSRF и permission, зачем нужен CSP и чем подтверждать отрицательный путь.",
"contentHtml": "<p>Симптом у старой админки простой: пользователь открывает форму, нажимает «Сохранить», и профиль меняется. Но тот же маршрут принимает запрос без CSRF-токена. В логах виден только действующий сеанс, поэтому команда принимает успешный ответ за доказательство безопасности. Цена ошибки зависит от права пользователя: посторонняя страница может заставить его браузер отправить действие с cookie, а запрос с известной сессией может изменить данные без явного намерения. Для администратора это может означать смену реквизитов, добавление пользователя или изменение настроек.</p>\n<p>Тезис статьи короткий: минимальный security baseline строится вокруг конкретного state change. Сначала сервер проверяет, кого он узнал. Затем проверяет контекст формы и право на действие. Только после этого меняет состояние. Cookie-флаги и CSP дополняют этот контур, но не заменяют ни CSRF-проверку, ни серверную авторизацию. Учебный пример ниже не доказывает защиту production-системы. Он показывает модель, которую можно проверить на одном маршруте.</p>\n<h2>Где возникает ошибка</h2>\n<p>Рассмотрим учебный маршрут <code>POST /admin/profile</code>. Он меняет только <code>displayName</code>. Браузер отправляет cookie с сессией автоматически. Если сервер решает «сессия есть — разрешаем», запрос с другого сайта выглядит для него почти так же, как запрос из формы админки. Поле <code>Origin</code> помогает проверить источник, а CSRF-токен добавляет значение, которого атакующий сайт не должен знать. Эти проверки относятся к контексту формы. Permission отвечает на другой вопрос: имеет ли распознанный пользователь право менять профиль.</p>\n<p>Порядок важен. Сначала отбрасываем неподходящий метод и маршрут. Потом получаем сессию. Для HTML-формы проверяем разрешённый origin и токен. Затем проверяем <code>profile:write</code>. Функция обновления не должна вызываться до последней проверки. Если permission спрятан только в интерфейсе, любой клиент сможет обратиться к endpoint напрямую. Если токен проверяется после update, защита появляется в отчёте, но не в поведении.</p>\n<figure><img src=\"/assets/editorial/2020/security-baseline-asset-threat-control-2020.svg\" alt=\"Связь актива, угрозы, контроля и доказательства для административного маршрута\"><figcaption>Минимальный контур связывает действие с угрозой, контролем и наблюдаемым доказательством. Успешный ответ проверяет доступность функции, а отрицательные запросы проверяют границы.</figcaption></figure>\n<h2>Один маршрут и несколько независимых контролей</h2>\n<p>У сессии, CSRF, permission и CSP разные владельцы. Сессия связывает cookie с субъектом. CSRF-контроль проверяет, что state change пришёл в ожидаемом контексте. Permission ограничивает действие субъекта. CSP ограничивает ресурсы, которые браузер может загрузить на странице. Нельзя заменить permission атрибутом <code>HttpOnly</code> или закрыть XSS одной директивой CSP.</p>\n<pre><code>function updateProfile(request) {\n if (request.method !== 'POST' || request.path !== '/admin/profile') {\n return { status: 404, reason: 'route-not-found' };\n }\n\n const session = sessions.find(request.cookies.sid);\n if (!session) return { status: 401, reason: 'unknown-session' };\n\n if (request.origin !== 'https://admin.example.test') {\n return { status: 403, reason: 'unexpected-origin' };\n }\n\n if (!csrf.verify(session.id, request.body.csrfToken)) {\n return { status: 403, reason: 'invalid-csrf-token' };\n }\n\n if (!session.permissions.includes('profile:write')) {\n return { status: 403, reason: 'insufficient-permission' };\n }\n\n profileStore.setDisplayName(session.userId, request.body.displayName);\n return { status: 204 };\n}</code></pre>\n<p>Код намеренно похож на обычный серверный псевдокод. В нём нет настоящей библиотеки сессий, способа выдачи токена или базы данных. Это учебный пример: он показывает границу между проверкой и побочным эффектом. В реальном приложении токен должен генерировать и проверять поддерживаемый механизм фреймворка или отдельная проверенная библиотека. Значение из примера нельзя копировать как секрет. Также нужно валидировать и безопасно выводить <code>displayName</code>; CSRF не защищает от XSS и некорректной обработки ввода.</p>\n<h2>Симптомы и действия</h2>\n<table><caption>Проверка одного state-changing маршрута</caption><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Любой POST с cookie получает успех</td><td>Сессия принята за доказательство намерения</td><td>Отправить запрос без токена и с чужим origin на тестовом стенде</td><td>Отклонять запрос до update и добавить отрицательный тест</td></tr><tr><td>Проверка права есть только в UI</td><td>Кнопка скрывает действие, но endpoint не авторизует его</td><td>Вызвать маршрут ролью <code>profile:read</code></td><td>Проверять <code>profile:write</code> на сервере</td></tr><tr><td>CSRF включён на одной форме</td><td>Защита привязана к компоненту, а не к карте изменений</td><td>Перечислить POST, PUT, PATCH и DELETE, которые меняют состояние</td><td>Для каждого маршрута определить контекст и механизм защиты</td></tr><tr><td>После CSP страница ломается</td><td>Policy добавили без списка реальных ресурсов</td><td>Сравнить нарушения с нужными script, style, image и frame</td><td>Начать с узкой policy, исправить зависимости и только потом включать enforcement</td></tr><tr><td>В отчёте есть cookie flags, но нет факта отказа</td><td>Настройку приняли за наблюдаемое поведение</td><td>Проверить response headers и четыре ветки решения</td><td>Хранить контракт ответа и положительный/отрицательные сценарии отдельно</td></tr></tbody></table>\n<h2>Cookie и CSP не должны маскировать главную проверку</h2>\n<p>Для сессии обычно задают <code>Secure</code> и <code>HttpOnly</code>. Первый ограничивает отправку cookie HTTPS-сценарием, второй не даёт обычному JavaScript прочитать значение. Узкий <code>Path</code> может уменьшить область отправки. Но ни один из этих атрибутов не говорит, что пользователь имеет право на конкретный update. Сервер всё равно должен разобрать сессию и выполнить permission check. Для критического действия могут потребоваться повторная аутентификация или одноразовое подтверждение.</p>\n<p>CSP задаёт браузеру список разрешённых источников и может уменьшить последствия инъекции или встраивания страницы. Учебный вариант можно начать так:</p>\n<pre><code>Content-Security-Policy:\n default-src 'self';\n script-src 'self';\n object-src 'none';\n base-uri 'self';\n frame-ancestors 'none'</code></pre>\n<p>Эта policy может сломать inline-скрипты, внешний analytics и embedded widget. Поэтому не добавляйте домены по одной ошибке в консоли. Сначала выпишите ресурсы конкретной страницы, проверьте, нужны ли они активу, и примените policy в режиме наблюдения, если это поддерживает ваша схема развёртывания. CSP не исправляет небезопасное экранирование, валидацию входа или доверие к пользовательскому HTML.</p>\n<h2>Проверяемый отрицательный путь</h2>\n<p>Положительный сценарий отвечает только на вопрос «маршрут работает». Для baseline важнее сохранить отказ там, где контроль должен сработать. На учебной среде нужны минимум четыре входа: корректная сессия, origin и токен с правом получают <code>204</code>; отсутствующий токен получает <code>403</code>; чужой origin получает <code>403</code>; роль без <code>profile:write</code> получает <code>403</code>. Не проверяйте этот пример на чужом сервисе и не используйте настоящие cookie или персональные данные.</p>\n<p>Логируйте причину отказа без токена и секретов. Для диагностики достаточно идентификатора маршрута, результата проверки и request id. Не записывайте значение CSRF-токена, session ID или полный body. Если реальный proxy переписывает заголовки, проверяйте также финальный HTTP-ответ разрешённого стенда. Unit-тест функции и проверка ответа веб-сервера доказывают разные вещи.</p>\n<h2>Порядок внедрения</h2>\n<ol><li>Назовите один актив и один маршрут, который меняет его состояние. Запишите клиентов маршрута: HTML-форма, API или интеграция.</li><li>Разделите решения: сессия, контекст формы, permission и response policy. Для каждого укажите вход, владельца и момент проверки.</li><li>Поставьте все server-side gates перед вызовом функции, которая меняет данные. Не полагайтесь на скрытую кнопку или проверку в JavaScript.</li><li>Подключите CSRF-механизм фреймворка, если он есть. Для собственной схемы отдельно опишите выдачу, срок жизни, привязку и сравнение токена.</li><li>Опишите cookie attributes и CSP для реального маршрута. Сверьте policy с ресурсами страницы, браузерами и конфигурацией веб-сервера.</li><li>Добавьте положительный тест и отрицательные тесты для пустого токена, чужого origin, неизвестной сессии и недостаточного permission.</li><li>Проверьте финальный ответ на разрешённом стенде. Сохраните только безопасные поля: статус, выбранный маршрут, тип отказа и request id.</li><li>Запишите непокрытые зоны: XSS, SQL injection, upload, CORS, rate limit, восстановление аккаунта, TLS, секреты, зависимости и сетевой периметр.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Этот baseline не является полным аудитом. Он не проверяет криптографию, конфигурацию CDN, реальный браузерный cache, доступность cookie на всех поддоменах, API-интеграции или права каждой роли. Origin может отсутствовать в некоторых допустимых контекстах, а один и тот же endpoint может обслуживать разные типы клиентов. В таком случае нельзя бездумно расширять исключения: нужно разделить контракты и описать отдельную модель угрозы.</p>\n<p>Работу можно считать готовой для одного маршрута, если документирован актив, а до изменения состояния проходят отдельные проверки сессии, контекста и permission; четыре учебных отрицательных/положительных сценария дают ожидаемые статусы; финальный ответ содержит согласованные cookie и CSP-поля; журнал не раскрывает секреты; команда явно записала непокрытые области. Это проверяемый критерий для узкого участка, а не заявление, что всё приложение защищено.</p>\n<h2>Проверяемые источники</h2>\n<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-защиты.</li><li><a href=\"https://owasp.org/www-project-application-security-verification-standard/\" target=\"_blank\" rel=\"noopener\">OWASP: Application Security Verification Standard</a> — официальный набор требований для проверки технических контролей веб-приложения.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy\" target=\"_blank\" rel=\"noopener\">MDN: Content-Security-Policy header</a> — синтаксис заголовка и назначение директив CSP.</li></ul>"
}