Files
progcode/editorial/agent-rewrites/063.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": 63,
"slug": "editorial-2026-04-practice-modern-web-security",
"title": "Безопасность веба начинается с границы: как доказать, что контроль прерывает атаку",
"excerpt": "CSP, CSRF-токен и проверка прав решают разные задачи. Разбираем путь атаки, точку прерывания, отрицательный тест и критерий, по которому защиту можно проверить.",
"contentHtml": "<p>Симптом часто выглядит убедительно: сервер отдаёт заголовок CORS, cookie помечена <code>HttpOnly</code>, а endpoint изменения профиля проверяет авторизацию. Но чужая страница всё ещё может отправить POST с cookie пользователя. Команда видит несколько включённых controls и считает задачу закрытой. Цена ошибки — изменение данных от имени жертвы, инцидент без понятной точки отказа и долгий спор о том, какая настройка должна была остановить запрос.</p>\n<p>Тезис статьи простой: контроль защищает не «веб вообще», а конкретный переход в маршруте атаки. Для каждой меры нужно назвать вход, условие, точку прерывания и проверяемый результат. CORS ограничивает чтение ответа браузером. CSRF-токен проверяет намерение для state-changing запроса. Проверка прав решает, может ли пользователь выполнить операцию. Эти меры дополняют друг друга, но одна не заменяет другую.</p>\n<h2>Сначала опишите путь атаки</h2>\n<p>Начните с действия нарушителя, а не со списка заголовков. В учебном сценарии пользователь вошёл в приложение, браузер хранит сессионную cookie, а endpoint принимает <code>POST /api/profile/email</code>. Внешняя страница содержит форму или JavaScript, который отправляет запрос на этот адрес. Браузер может приложить cookie к запросу. Если сервер не требует отдельного доказательства намерения, запрос меняет email.</p>\n<p>У пути есть четыре наблюдаемые точки: источник запроса, браузер, endpoint и операция записи. CORS не делает внешний POST невозможным. Он обычно мешает прочитать ответ из JavaScript. Это другая граница. <code>HttpOnly</code> не запрещает браузеру отправлять cookie. Он только скрывает cookie от JavaScript. SameSite может уменьшить риск для части кросс-сайтовых запросов, но режим зависит от контекста, способа навигации и политики cookie. Защита должна проверять контракт на сервере.</p>\n<figure><img src='/assets/editorial/2026/modern-web-security-2026-attack-control-evidence-map.svg' alt='Карта пути атаки в вебе: внешний запрос проходит через браузер к endpoint, а проверка CSRF и прав прерывает разные переходы' loading='lazy' /><figcaption>Один путь атаки может пересекать несколько границ. Каждая проверка должна иметь свою точку прерывания и собственный отрицательный тест.</figcaption></figure>\n<h2>Механизм: разные controls закрывают разные переходы</h2>\n<p>Аутентификация отвечает на вопрос «кто отправил запрос?». Авторизация отвечает на вопрос «может ли этот пользователь изменить этот объект?». CSRF-защита отвечает на вопрос «есть ли у запроса доказательство, которое внешний сайт не может получить и воспроизвести?». Валидация входа отвечает на вопрос «соответствует ли значение контракту поля?». Нельзя перенести ответ одного слоя на другой.</p>\n<p>Сервер может принять валидный токен CSRF от пользователя без права менять чужой профиль. Тогда токен доказал происхождение запроса, но не право на операцию. Обратный случай тоже возможен: сервер правильно проверяет право, но принимает запрос с cookie без CSRF-защиты. Злоумышленник не получает новые права, но заставляет уже авторизованного пользователя выполнить разрешённое действие.</p>\n<p>Контроль становится проверяемым, когда его условие видно в коде и в тесте. Фраза «CORS настроен» не сообщает, что именно проверяли. Формулировка «без заголовка <code>X-CSRF-Token</code> endpoint возвращает 403 и не меняет запись» задаёт границу. Она не доказывает безопасность всех endpoint-ов, но доказывает один отрицательный путь для одной операции.</p>\n<h2>Учебный пример: серверная граница для записи</h2>\n<p>Ниже — учебный фрагмент на Express-подобном API. Он не подключается к базе, не создаёт настоящую сессию и не показывает production-результат. Функции <code>getSession</code>, <code>findUser</code> и <code>updateEmail</code> обозначают границы приложения. В реальном сервисе их контракты нужно проверить отдельно.</p>\n<pre><code>app.post('/api/profile/email', async (req, res) =&gt; {\n const session = await getSession(req);\n if (!session) return res.sendStatus(401);\n\n const csrf = req.get('X-CSRF-Token');\n if (!csrf || !timingSafeEqual(csrf, session.csrfToken)) {\n return res.sendStatus(403);\n }\n\n const email = parseEmail(req.body.email);\n if (!email) return res.status(400).json({ error: 'invalid_email' });\n\n const user = await findUser(session.userId);\n if (!user || user.id !== session.userId) return res.sendStatus(403);\n\n await updateEmail(user.id, email);\n return res.sendStatus(204);\n});</code></pre>\n<p>Порядок проверок здесь не является универсальным шаблоном. Он показывает цепочку решений: сессия допускает запрос в контекст пользователя, токен закрывает CSRF-переход, парсер ограничивает значение, а проверка идентификатора не даёт обновить чужую запись. Каждый отказ имеет отдельный статус и тестируемое условие. Если приложение использует другой способ передачи CSRF-токена, сохраняется тот же принцип: внешний сайт не должен получить нужное доказательство из обычной сессии.</p>\n<p>Отрицательный путь обязателен. Уберите заголовок, оставьте cookie и отправьте тот же POST. Ожидаемый результат — 403, а значение email не изменилось. Если сервер вернул 204 или запись изменилась, CORS, <code>HttpOnly</code> и наличие формы входа не имеют значения: граница endpoint пропускает запрос. В учебном примере это проверка логики, а не свидетельство поведения конкретного production-сервиса.</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>Есть CORS, но чужая форма меняет данные</td><td>CORS ограничивает чтение ответа, а не сам state-changing запрос</td><td>Отправить POST без CSRF-токена и проверить статус и запись</td><td>Добавить серверную проверку CSRF или иной эквивалентный механизм</td></tr><tr><td>Cookie имеет HttpOnly</td><td>Браузер всё ещё может приложить cookie к запросу</td><td>Проверить фактический запрос во внешнем контексте</td><td>Не считать HttpOnly защитой от CSRF; оставить его для защиты от чтения cookie скриптом</td></tr><tr><td>Токен проверяется, но меняется чужой объект</td><td>CSRF-токен не заменяет авторизацию</td><td>С токеном пользователя запросить объект другого пользователя</td><td>Сверить владельца объекта с субъектом сессии и вернуть 403</td></tr><tr><td>Валидация поля есть только в браузере</td><td>Клиентский код не является доверенной границей</td><td>Отправить запрос напрямую с неверным или лишним полем</td><td>Повторить валидацию на сервере до записи</td></tr><tr><td>CSP включена, но XSS-тест проходит</td><td>Политика не исправляет небезопасный sink и неверное происхождение HTML</td><td>Проверить response header и отрицательный сценарий для конкретного sink</td><td>Исправить источник и вывод данных; использовать CSP как дополнительный слой</td></tr></tbody></table></div>\n<h2>Как связывать control и evidence</h2>\n<p>Для каждой меры заведите короткую карточку. В поле <code>asset</code> назовите защищаемый объект. В <code>precondition</code> запишите условие, при котором атака возможна. В <code>interruption</code> укажите запрещаемый переход. В <code>evidence</code> положите наблюдение, которое можно повторить. Последнее поле должно иметь границу: оно отвечает только на свой вопрос.</p>\n<pre><code>{\n &quot;asset&quot;: &quot;profile.email&quot;,\n &quot;precondition&quot;: &quot;session_cookie_present&quot;,\n &quot;attackPath&quot;: [\n &quot;external_page&quot;,\n &quot;browser_attaches_cookie&quot;,\n &quot;POST_profile_email&quot;,\n &quot;profile_changed&quot;\n ],\n &quot;interruption&quot;: &quot;reject_without_csrf_token&quot;,\n &quot;evidence&quot;: &quot;same_request_returns_403_and_value_is_unchanged&quot;,\n &quot;notProven&quot;: [\n &quot;authorization_for_other_objects&quot;,\n &quot;all_other_write_endpoints&quot;,\n &quot;XSS_protection&quot;\n ]\n}</code></pre>\n<p>Такая запись полезнее поля <code>security: enabled</code>. Она показывает, что именно проверяли и что осталось за пределами проверки. Если evidence не связывает asset, endpoint и отрицательный результат, его нельзя переносить на другой маршрут. Если в карте нет residual risk, это не означает нулевой риск. Это означает, что карту заполнили неполно.</p>\n<h2>Почему CSP и CSRF нельзя смешивать</h2>\n<p>Content Security Policy (CSP) задаёт браузеру правила для ресурсов и выполнения скриптов. Она может уменьшить последствия инъекции контента. Но CSP не знает, имеет ли пользователь право менять email, и не добавляет секретный токен в POST. Поэтому политика может быть полезной дополнительной защитой, но не заменяет серверную проверку намерения и прав.</p>\n<p>Обратное ограничение тоже важно. Хорошая CSRF-защита не исправляет HTML-инъекцию. Если пользовательский текст попадает в небезопасный DOM-sink, скрипт может выполнить действие уже из доверенного контекста страницы и прочитать доступные данные. В этом случае нужны безопасный вывод, кодирование, ограничения источников и тесты для конкретного sink. Нельзя закрыть XSS, добавив заголовок к endpoint изменения профиля.</p>\n<h2>Порядок действий</h2>\n<ol><li>Выберите одну операцию записи: endpoint, HTTP-метод, ресурс и изменяемое поле.</li><li>Опишите симптом и цену ошибки: что изменится, если внешний запрос пройдёт.</li><li>Нарисуйте упорядоченный путь от источника запроса до побочного эффекта.</li><li>Для каждого control назовите один переход, который он должен прервать.</li><li>Проверьте серверную аутентификацию, авторизацию, CSRF и валидацию независимо.</li><li>Сделайте отрицательный запрос: уберите токен, поменяйте владельца или подставьте неверное поле.</li><li>Проверьте не только статус ответа, но и состояние ресурса после отказа.</li><li>Запишите evidence и отдельный список того, что тест не доказывает.</li><li>Повторите проверку после изменения middleware, cookie-политики, маршрута или формата запроса.</li></ol>\n<h2>Ограничения</h2>\n<p>CSRF-токен защищает конкретный контракт, если сервер действительно проверяет его до изменения состояния. Он не защищает от украденной сессии, вредоносного скрипта внутри доверенного origin или компрометации сервера. SameSite-cookie снижает риск для части сценариев, но её поведение зависит от браузера и контекста. Не делайте из свойства cookie универсальное доказательство.</p>\n<p>Код примера не покрывает OAuth callback, загрузку файлов, WebSocket, GraphQL mutations и фоновые очереди. У каждого канала свои границы. Для GraphQL нужно проверить mutation и resolver. Для файла — имя, тип, содержимое, место хранения и выдачу. Для очереди — кто помещает сообщение, кто его обрабатывает и повторяется ли операция безопасно.</p>\n<p>Не объявляйте защиту готовой по одному зелёному тесту. Тест может подтвердить отказ без токена, но не проверит права на чужой объект или другой endpoint. Проверка готова, когда исходный отрицательный запрос возвращает ожидаемый отказ, состояние ресурса не изменяется, а запись связывает результат с конкретным маршрутом и перечисляет непроверенные соседние пути.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Для выбранной операции у команды есть карта из asset, precondition, attack path, interruption и evidence. Есть автоматизированный или воспроизводимый тест без нужного доказательства. Он получает отказ, а запись остаётся неизменной. Отдельный тест проверяет права на чужой объект. В документе явно указано, что CORS, HttpOnly и CSP не заменяют эти проверки. Если хотя бы одного пункта нет, результат — не «защищено», а «нужна следующая проверка».</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://www.w3.org/TR/2026/WD-CSP3-20260421/' target='_blank' rel='noopener noreferrer'>W3C: Content Security Policy Level 3</a> — нормативное описание CSP и её роли как дополнительного слоя против инъекции контента. Документ не доказывает настройку конкретного сайта.</li><li><a href='https://github.com/OWASP/ASVS/blob/5cf9b032440be53ce345ab3c130fda46ba1ce7a2/5.0/en/0x12-V3-Web-Frontend-Security.md' target='_blank' rel='noopener noreferrer'>OWASP Application Security Verification Standard 5.0.0: Web Frontend Security</a> — официальный набор проверяемых требований. Требование стандарта не является результатом оценки конкретного приложения.</li><li><a href='https://doi.org/10.6028/NIST.IR.8397' target='_blank' rel='noopener noreferrer'>NISTIR 8397: Guidelines on Minimum Standards for Developer Verification of Software</a> — официальный документ о сочетании threat modeling, автоматизированных тестов, статического анализа и других техник. Он не заменяет evidence для отдельного endpoint.</li></ul>"
}