Піддомен адмінки не зупиняє 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 лише для хоста, токен на кожну зміну, вхідний фільтр, якому ви не довіряєте як особі, і авторизація, визначена для кожного запиту.