Получили токен с сервера — куда его положить, чтобы потом доставать? Два главных варианта: куки и веб-хранилища (localStorage / sessionStorage). У обоих свои плюсы, минусы и сценарии атак. Разберём — и сделаем осознанный выбор, а не «куда первым научились, туда и пихаю».

localStorage / sessionStorage

Самое простое: вызов localStorage.setItem('token', '...'), и токен лежит в браузере до конца света (sessionStorage — до закрытия вкладки).

Плюсы:

  • Тривиальный API, известен всем.
  • JS легко достаёт токен и сам кладёт в Authorization-заголовок — полностью контролируемо.
  • Никаких сюрпризов с CSRF (об этом ниже): браузер сам ничего не прикрепляет к запросам.

Минусы:

  • Доступно из любого JS на странице. А значит — уязвимо к XSS: если злоумышленник внедрил скрипт (например, через комментарий или сторонний пакет в node_modules) — он мгновенно вытащит ваш токен и улетит с ним к себе на сервер.
  • Сохраняется между вкладками и сессиями. Если устройство общее — токен живёт после ухода пользователя.

HttpOnly Secure куки

Сервер ставит куку в ответе:

Set-Cookie: refreshToken=fF8a2d...; HttpOnly; Secure; SameSite=Strict; Path=/api

Разбор флагов:

  • HttpOnly — куку нельзя прочитать из JavaScript. Никакой document.cookie её не увидит. Браузер прикрепляет к запросам на тот же домен сам, JS не знает значения.
  • Secure — кука уходит только по HTTPS. По обычному HTTP не передаётся.
  • SameSite=Strict | Lax | None — правила прикрепления к запросам с других сайтов (защита от CSRF). Strict — не прикреплять никогда с других сайтов; Lax — только на «безопасные» навигации (клик по ссылке); None — прикреплять всегда (требует Secure).
  • Path=/api — куку прикреплять только к запросам на пути /api и ниже.

Плюсы:

  • XSS не может прочитать токен — HttpOnly его прячет от JS.
  • JS не нужно вручную прикреплять токен — куки браузер сам подставляет.

Минусы:

  • CSRF: если кука прикрепляется автоматически, злоумышленник может с другого сайта сделать запрос на ваш бэк — и кука улетит вместе с ним. Защита: SameSite=Strict/Lax + CSRF-токен в дополнительном заголовке.
  • Работает только в рамках своего домена. Третьесторонние API через куки авторизовать сложно.

Что такое XSS и CSRF одной фразой каждый

  • XSS (Cross-Site Scripting) — злоумышленник запустил свой JS на вашем сайте (например, через невычищенный пользовательский ввод). Может всё, что может ваш JS: прочитать localStorage, делать запросы от вашего имени, перехватывать клики.
  • CSRF (Cross-Site Request Forgery) — злоумышленник со своего сайта обманом вызвал запрос на ваш бэк от имени пользователя. Браузер сам прикрепил куку. Сервер не знает, что запрос пришёл не из легитимного интерфейса.

HttpOnly-куки защищают от XSS-кражи токена, но открыты CSRF. localStorage защищён от CSRF (браузер ничего не прикрепляет сам), но уязвим к XSS.

Что выбрать на практике

Прагматичный современный паттерн:

  1. Refresh token — всегда в HttpOnly Secure SameSite=Strict куке. Это самый ценный токен (живёт неделями), его потеря катастрофична. XSS его украсть не может.
  2. Access token — в памяти JS (переменная в модуле, контекст React). При перезагрузке страницы пропадает — не страшно, сразу делаем refresh и получаем новый.
  3. В localStorage токены не кладём вообще — даже короткоживущий access. XSS его перехватит за секунду.

Если бэкенд не поддерживает refresh через куки и у вас нет выбора — access кладёте в sessionStorage (хотя бы умрёт с закрытием вкладки), и яростно защищаетесь от XSS на уровне фронта: CSP, sanitization, никаких innerHTML с пользовательским контентом.

SameSite=Strict защищает почти от всего CSRF

Раньше CSRF-токены были обязательной частью защиты — сервер ставил один-разовый токен в куку, фронт его читал и прикреплял в заголовок к каждому небезопасному запросу. Сервер сверял.

Сегодня SameSite=Strict закрывает большинство CSRF-сценариев сам. Кука не уйдёт ни с одного стороннего сайта. CSRF-токен остаётся как пояс с подтяжками, но в современных приложениях постепенно уходит.

Резюме

Куда положить XSS CSRF Когда
localStorage уязвим защищён почти никогда — лучше избегать
sessionStorage уязвим защищён fallback, если нет HttpOnly-куки
JS-переменная в памяти уязвим защищён хорошее место для access token
HttpOnly Secure cookie защищён уязвим без SameSite лучшее место для refresh token