Скачать CV

Security

Токену в JavaScript не место

Sanctum пишет auth_token как httpOnly. В JSON логина нет поля token. Если страница читает сессию, XSS её унесёт.

В первом JSON логина на этом API было поле token. Vue его сохранил. Тело ещё попало в access-лог nginx и в network-панель браузера. localStorage читает любой скрипт на origin. Одной внедрённой зависимости или одного неэкранированного поля хватает, чтобы токен ушёл, и он живёт до истечения срока.

Cookie с httpOnly из JavaScript не достать. Та же инъекция не прочитает auth_token. Маршрут логина теперь ставит cookie с httpOnly, secure и SameSite lax. API и Vue SSR на одном сайте, браузер шлёт cookie без игр с CORS credentials. В JSON успешного логина нет ключа token.

Пользователям строка не была нужна. XSS была нужна. Хосту нужно одно хранилище сессии, не копия в JS и копия в cookie.

Не кладите токен в JSON. Логи и состояние клиента сохранят копию.
Не кладите токен в JSON. Логи и состояние клиента сохранят копию.

CSRF всё ещё нужен

Cookie едет на любом запросе браузера к этому сайту. XSS cookie не прочитает. Форма на другом сайте всё ещё может вызвать запись, если CSRF не стоит на пути. Sanctum читает CSRF-cookie и ждёт заголовок на небезопасных методах.

Logout удаляет токен на сервере. Стереть cookie в браузере мало. Украденная cookie, которая ещё лежит в Redis или в таблице токенов, продолжит работать. После logout /api/user должен отдать 401.

Я нашёл хелпер, который после логина клал объект пользователя в sessionStorage, включая ключ token со старой формы ответа. Cookie уже была httpOnly. Хелпер вернул утечку. В storage остаётся имя и локаль, ничего, чем вызвать /api.

Держите cookie вместе с CSRF. Sanctum ждёт заголовок на небезопасных методах.
Держите cookie вместе с CSRF. Sanctum ждёт заголовок на небезопасных методах.

Тесты, которые это держат

Четыре проверки ловят большую часть регрессий, которые я тут видел. Логин не возвращает поле token. У cookie флаг httpOnly. Запись без CSRF-заголовка отклоняется. После logout эндпоинт пользователя это 401.

Фронтенд никогда не пишет auth_token в localStorage, sessionStorage или persist у Pinia. Если следующий хелпер положит объект пользователя в storage, в объекте не должно быть токена. Сессия это cookie. Всё остальное это имя на экране.

SSR не должен печатать cookie в состояние страницы. Сериализованный пользователь в HTML нормален. Сериализованный секрет это та же утечка, что localStorage, с более длинной жизнью в кэше, если nginx когда-нибудь сохранит этот HTML.

Какой опыт из этого

Грепаю JSON логина, persist Pinia и payload SSR на поле token. Логи и HTML это копии ответа. Если строка там, XSS не нужен localStorage.

Logout, который только чистит cookie в браузере, я не доверяю. Строка на сервере должна умереть. Одному SameSite не верю. CSRF всё ещё стоит на небезопасных методах.

Правило, которое оставляю: только cookie Sanctum, httpOnly, никогда в JSON. Фронтенд хранит имя на экране. /api/user после logout это 401.

К заметкам