{ "index": 258, "slug": "editorial-2020-11-practice-security-baseline", "title": "Минимальная защита старой админки: один маршрут, четыре контроля", "excerpt": "Не начинаем с коллекции заголовков: для учебного маршрута связываем актив, угрозу, контроль и наблюдаемое доказательство.", "contentHtml": "
Симптом у старой админки обычно выглядит безобидно: форма меняет профиль, сессия работает, а после релиза никто не может показать, какие условия действительно защищают это изменение. В настройке лежат несколько headers, в коде есть проверка пользователя, но связь между активом, угрозой и проверкой не записана. Цена такой неопределённости конкретна: команда может добавить ещё один заголовок, оставить state-changing маршрут без server-side permission или принять успешный ответ за доказательство, что посторонний запрос не изменит данные.
\nВ ноябре 2020 года я бы начал не с большого аудита и не с обещания «закрыть web security». Ниже — учебный маршрут POST /admin/profile на вымышленном origin https://admin.example.test. Его предполагаемый эффект — изменение synthetic display name; fixture останавливается раньше, на решении до эффекта. Цель — собрать маленький baseline: известная сессия, токен и origin для state change, permission на сервере, cookie contract и ограничивающий CSP header. Это не pentest, не security scan и не подтверждение production-защиты.
Актив здесь не «сайт вообще», а возможность изменить профиль из административной формы. Угроза — запрос приходит не в ожидаемом пользовательском сценарии или пользователь с действующей сессией вызывает действие без нужного права. Контроль — не одно условие, а несколько независимых: сервер узнаёт сессию, для формы сравнивает CSRF token и ожидаемый origin, затем проверяет permission перед побочным эффектом. Доказательство — фиксированный отрицательный сценарий, который получает 403, и явный положительный сценарий, который получает 204.
Такое сужение важно. Cookie с HttpOnly полезен для ограничения доступа скрипта к значению cookie, но сам по себе не отвечает на вопрос, можно ли отправить нежелательное действие из браузера. Аналогично CSP ограничивает источники контента на стороне user agent, но не заменяет validation input, encoding output и authorisation в приложении. OWASP ASVS 4.0.2 уже был доступен в октябре 2020 года; я использую его как список проверяемых областей, а не как наклейку «соответствует стандарту».
| Актив | Угроза в узком маршруте | Контроль | Что считаем доказательством |
|---|---|---|---|
| Изменение display name | запрос проходит без ожидаемого контекста формы | сравнить Origin и CSRF token до update | fixture отклоняет foreign origin и пустой token с 403 |
| Сессия администратора | значение cookie доступно скрипту или посылается без HTTPS-контракта | Secure, HttpOnly, узкий Path=/admin | учебная карта response headers содержит три атрибута |
| Право на изменение | аутентифицированный пользователь пишет без роли | server-side profile:write перед эффектом | fixture отклоняет profile:read с 403 |
| Разметка admin page | инъекция загружает неожиданный script или объект | validation/encoding в приложении плюс CSP2 policy | в карте headers есть default-src, object-src и frame-ancestors |
В строке про сессию Path — ограничение области отправки cookie, а не граница авторизации. RFC 6265 прямо описывает механизмы Cookie и Set-Cookie; из этого не следует, что cookie attribute заменяет серверную проверку права. В строке про CSP также нет обещания «остановить XSS». W3C CSP Level 2 называет policy дополнительной защитой: сначала приложение корректно работает с input и output, затем browser получает более узкий список допустимых источников.
Симптом «форма сработала» ничего не говорит о порядке проверок. Причина ошибки часто в том, что разработчик смешивает наличие сессии с правом на конкретное действие. Проверка должна быть простой: до вызова функции обновления собрать method, path, session, origin, token и permission; затем явно остановить маршрут на первом неподходящем условии. Действие — оставить этот порядок рядом с кодом и не переносить его в неочевидный клиентский обработчик.
\n// Учебный псевдокод одного HTML-маршрута. Не middleware и не framework API.\nconst decision = evaluateTrainingStateChange({\n method: 'POST',\n path: '/admin/profile',\n sessionId: request.cookie.sid,\n origin: request.header.origin,\n csrfToken: request.form.csrf,\n permission: request.user.permission,\n});\n\nif (!decision.allowed) return response.status(decision.status).end();\nreturn updateTrainingProfile(request.form.displayName).status(204).end();\nЭто намеренно не готовое middleware и не код какого-либо framework. В учебной форме token передаётся в body, а origin сравнивается только для одного HTML state-changing маршрута. В реальном приложении надо отдельно решить, какие клиенты существуют, когда browser посылает Origin, как выдаётся token, где хранится сессия и как API отличается от формы. Но permission всё равно остаётся серверной границей: клиентский скрытый элемент, URL и кнопка не являются доказательством права.
\nСимптом у конфигурации другой: header записан в wiki, но никто не знает, на каком route и в каких ответах его ждут. Причина — security policy живёт отдельно от маршрута. Проверка — сделать маленькую карту response headers и привязать её к /admin/, а не включать случайный набор директив глобально. Действие — перед применением сверить syntax с версией веб-сервера, инвентаризировать фактические scripts, styles и frames, а затем проверить ответ разрешённого стенда.
# Учебный фрагмент, не готовая конфигурация production-сервера.\n# admin.example.test — учебный origin; внешний адрес или секрет здесь не нужны.\nlocation /admin/ {\n add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;\n add_header Referrer-Policy "same-origin" always;\n proxy_pass http://training_admin_upstream;\n}\n\n# Атрибуты cookie выставляет приложение после успешной аутентификации:\n# Set-Cookie: sid=...; Path=/admin; Secure; HttpOnly\nВ примере policy запрещает plugin content, ограничивает base URL и запрещает встраивание страницы в frame. Она также разрешает scripts только с того же origin. Это может сломать старую страницу с inline script, внешним widget или другим asset host. Поэтому фрагмент не надо копировать в production «как есть»: сначала составляют список необходимых ресурсов, фиксируют допустимый риск и проверяют выбранную версию nginx. В статье нет реального proxy, upstream или HTTP-запроса; конфигурация показывает форму контракта, а не выполненную настройку.
\nДля маршрута полезно хранить не только код, но и один ожидаемый обмен. Симптом неопределённости — спор о том, должен ли route требовать origin, token или permission. Причина — контроль не выражен наблюдаемыми полями. Проверка — описать разрешённый учебный request и expected response headers. Действие — превратить пару в тест или безопасную ручную проверку на разрешённом стенде, когда такой стенд появится.
\nPOST /admin/profile HTTP/1.1\nHost: admin.example.test\nOrigin: https://admin.example.test\nCookie: sid=training-session-42\nContent-Type: application/x-www-form-urlencoded\n\ndisplayName=Alex&csrf=training-csrf-6fd2\n\nHTTP/1.1 204 No Content\nContent-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'\nSet-Cookie: sid=training-session-42; Path=/admin; Secure; HttpOnly\nReferrer-Policy: same-origin\nЗдесь нет реального session ID, адреса, пользователя или данных профиля. admin.example.test — учебный origin, а token намеренно короткий и не предназначен для защиты чего-либо вне fixture. Статус 204 означает лишь успешный исход синтетического маршрута. Нельзя из него сделать вывод, что TLS настроен, cookie реально доставляется браузером, CSP принят каждым user agent или сервер выдал headers на ошибке. Эти вопросы относятся к следующей, уже разрешённой проверке окружения.
Симптом регрессии проявится, если после следующей правки один из gates перестанет участвовать в решении. Причина может быть в перестановке middleware, другом имени поля или слишком широком условии. Проверка не требует подключаться к серверу: fixture меняет по одному training input и сравнивает решение. Действие — запускать её вместе с revision и не называть результатом этой команды проверку реального browser, proxy или security perimeter.
\n# В памяти: сеть, browser, proxy и scanner не запускаются.\nnode web/scripts/upgrade-2020-11.mjs --verify-fixture\n\n# Ищем не уязвимость, а сохранение учебного контракта:\n# expectedRequestAccepted=true\n# foreignOriginRejected=true\n# missingTokenRejected=true\n# insufficientPermissionRejected=true\n# cspContractPresent=true\n# sessionCookieContractPresent=true\nВ этой fixture foreign origin, пустой token и роль profile:read должны завершиться 403. Успешный synthetic request получает 204. Отдельно проверяется только наличие заданных полей CSP и Set-Cookie в объекте response contract. Если ответ на реальном стенде будет отличаться, это не повод менять fixture под сервер: сначала нужно назвать, какая граница модели отличается, и решить, расширяем ли contract либо обнаружили ошибку rollout.
Этот baseline не делает приложение «безопасным». Он не проверяет TLS, password storage, account recovery, file upload, SQL injection, logging, dependency vulnerabilities, rate limit, CORS, реальные браузеры или внешний сетевой периметр. Он также не запускает nginx и не утверждает, что header дошёл до production. Его результат скромнее и полезнее: для одного учебного изменения известны актив, угроза, control и evidence. Следующий инженер сможет расширить матрицу, не выдавая эту первую точку за полный AppSec-процесс.
\nВсе origin, cookies, токены, заголовки, имена маршрутов и результаты ниже учебные. Модуль не открывает сеть, не сканирует цели, не проверяет реальный сервер и не хранит секреты.
\n