Поддомен админки не останавливает CSRF: четыре проверки перед запуском
Новое имя хоста — это новый origin, часто тот же сайт, поэтому same-site cookie и простые формы его пересекают. Проверьте токены, шлюз и авторизацию.
Перенос административного интерфейса на admin.example.com — реальное улучшение: он держит админские cookie подальше от публичных страниц, даёт обоим приложениям отдельные кеши и позволяет направить более строгую сетевую политику на одно имя хоста. Но это не защита от CSRF и не делает код публичного сайта безопасным для открытого доступа вошедшего администратора. Разделение меняет то, что браузер считает границей, а не то, что проверяет сервер, поэтому выполните эти четыре проверки, прежде чем считать дело закрытым.
Читайте origin и сайт как два разных слова
Origin — это схема, хост и порт, поэтому https://www.example.com и https://admin.example.com — разные origin. Сайт в смысле SameSite — это схема плюс регистрируемый домен, поэтому эти два имени хоста обычно один сайт. В этом вся ловушка: атрибут SameSite не поможет между соседними поддоменами, потому что браузер вовсе не считает запрос межсайтовым. Направьте админский интерфейс на действительно другой регистрируемый домен, например admin-example.net, и запрос действительно станет межсайтовым, и именно там SameSite=Lax или Strict начинает делать работу, на которую можно положиться.
Проверьте это в браузере, а не рассуждениями.
Загрузите публичный сайт, откройте инструменты разработчика и посмотрите, какие cookie браузер прикладывает к запросу на админский хост. Если у cookie нет атрибута Domain, она только для хоста и остаётся на своём имени; если она несёт Domain=example.com, она уходит к каждому соседу, и новый поддомен ничего не дал для этой cookie.
Отделите чтение ответа от отправки запроса
Политика одного origin ограничивает чтение ответов между origin. Она не мешает отправить запрос и не мешает форме нести учётные данные, которые у браузера уже есть. Простой запрос, с типом содержимого формы или обычным GET, доставляется без предварительной проверки; затем CORS решает, может ли вызывающий скрипт читать ответ. CSRF живёт целиком на стороне отправки.
Поэтому серверу нужна явная проверка для операций, меняющих состояние. Токен синхронизации, привязанный к сессии, или cookie двойной отправки, сравниваемая на сервере, закрывают этот случай; сравнение Origin или Referer стоит добавить вторым шлюзом, но не единственным, потому что не каждый запрос несёт заголовок и не каждый прокси его сохраняет. Руководство OWASP по предотвращению CSRF считает SameSite одним слоем среди нескольких именно по этой причине.
Решите, что на самом деле доказывает сетевой шлюз
Список разрешённых выходов VPN перед админским хостом — хороший контроль и слабое утверждение о личности. Он доказывает, что запрос пришёл с адреса, который выдаёт VPN, который могут делить все на этом выходе, и ничего не говорит о том, какой человек или устройство стоит за ним. Относитесь к нему как к входному фильтру, а не как к аутентификации.
Держите его независимым от проверок на уровне запроса. Если список — единственное между атакующим и конечной точкой, меняющей состояние, один неверно настроенный маршрут, один клиент split-tunnel или один одолженный ноутбук полностью снимает контроль.
Записывайте и отказы извне ожидаемых диапазонов. Их всплеск — самый полезный сигнал, что кто-то прощупывает только что построенную границу, а тишина — сигнал, что фильтр вовсе не достигается.
Держите cookie, вход и авторизацию явными
Область cookie решает большую часть результата. Cookie родительского домена отправляется на админский хост, поэтому внедрение скрипта на публичном сайте может действовать с сессией администратора; cookie только для хоста не может. Предпочитайте форму только для хоста, добавьте атрибут Secure и используйте префикс __Host- там, где платформа его поддерживает, потому что этот префикс требует Secure, Path=/ и отсутствия атрибута Domain. Читайте заголовок ответа, а не банку cookie: Set-Cookie: __Host-admin_session=...; Secure; Path=/; HttpOnly; SameSite=Lax, без атрибута Domain. Браузер отбрасывает __Host- cookie с Domain, поэтому cookie, которая всё ещё видна на соседних хостах, — доказательство, что префикс так и не применился.
Затем держите авторизацию на сервере, для каждого запроса, на каждой админской конечной точке, а не только при входе. Запрос из разрешённой сети всё равно должен быть аутентифицирован и авторизован для запрошенной операции; имя хоста не является действующим лицом. Если публичное и админское приложения делят поток входа или хранилище сессий, запишите эту связь, потому что это кратчайший путь от ошибки на публичном сайте до административного действия.
Проверьте границу заново после разделения
Прогоните негативные тесты, а не только счастливый путь: межсайтовая форма без токена должна быть отклонена, токен из другой сессии должен быть отклонён, и ни один GET не должен менять состояние. Проверьте атрибуты cookie в инструментах разработчика, убедитесь, что проверка Origin срабатывает на несовпадающем запросе, и попробуйте админский хост извне диапазона VPN, чтобы увидеть фильтр в работе.
Ничто из этого не устраняет XSS-дефект внутри самого админского приложения. Отдельное имя хоста — граница изоляции на стороне браузера с реальной ценностью, и она не заменяет предотвращение небезопасного выполнения кода или защиту запросов на стороне сервера. После разделения список короче, чем кажется: отдельный origin, cookie только для хоста, токен при каждом изменении, входной фильтр, которому вы не доверяете как личности, и авторизация, решаемая для каждого запроса.