8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"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>"
|
||
}
|