Очистка чувствительных данных

Клиентское кеширование значительно ускоряет работу приложений, однако вместе с производительностью появляется риск хранения конфиденциальной информации в памяти браузера. TanStack Query активно кеширует ответы сервера, повторно использует результаты запросов и автоматически синхронизирует данные между компонентами. Если в кеше оказываются токены авторизации, персональные данные, финансовая информация или закрытые API-ответы, это создаёт потенциальную поверхность атаки.

К чувствительным данным обычно относятся:

  • JWT и refresh token
  • session id
  • cookies и CSRF-токены
  • персональные данные пользователя
  • email, телефон, адрес
  • банковская информация
  • внутренние идентификаторы
  • данные административных панелей
  • приватные настройки аккаунта
  • медицинские данные
  • результаты внутренних API

Даже если приложение использует HTTPS, чувствительные данные в памяти браузера остаются доступными:

  • через XSS-уязвимости
  • через сторонние расширения браузера
  • через DevTools
  • через дампы памяти
  • через сохранённый persist-кеш
  • через вкладки браузера после logout

По этой причине TanStack Query требует отдельной стратегии очистки кеша и контроля жизненного цикла данных.


Где TanStack Query хранит данные

Внутри библиотеки используется объект QueryCache, содержащий все query-записи. Пока query существует в кеше, её данные доступны через:

queryClient.getQueryData()

или через Devtools.

Даже после размонтирования компонента данные могут оставаться в памяти:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 10
    }
  }
})

В данном примере кеш хранится 10 минут после того, как перестал использоваться.

Для чувствительных данных такой подход часто недопустим.


Очистка кеша после logout

Наиболее важный сценарий — удаление пользовательских данных после выхода из системы.

Неправильная реализация:

const logout = async () => {
  await api.post('/logout')
}

В этом случае:

  • кеш TanStack Query сохраняется
  • старые данные доступны после logout
  • другой пользователь может увидеть предыдущую сессию
  • данные могут восстановиться автоматически

Правильная очистка:

const logout = async () => {
  await api.post('/logout')

  queryClient.clear()
}

Метод:

queryClient.clear()

полностью очищает:

  • query cache
  • mutation cache
  • observers
  • подписки

Это наиболее жёсткий и безопасный вариант.


Разница между clear, removeQueries и resetQueries

Полная очистка

queryClient.clear()

Удаляет всё.

Используется:

  • при logout
  • смене пользователя
  • критических ошибках авторизации
  • сбросе приложения

Удаление конкретных запросов

queryClient.removeQueries({
  queryKey: ['profile']
})

Удаляются только указанные query.

Подходит:

  • для чувствительных endpoint
  • при обновлении permission
  • после удаления аккаунта

Сброс состояния query

queryClient.resetQueries({
  queryKey: ['profile']
})

Сбрасывает query в initial state.

Отличия:

  • query остаётся зарегистрированной
  • observers сохраняются
  • возможен повторный refetch

Удаление данных после истечения сессии

Если backend возвращает 401, необходимо немедленно удалить чувствительные данные.

Пример глобального обработчика:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false
    }
  },
  queryCache: new QueryCache({
    onError: (error) => {
      if (error.response?.status === 401) {
        queryClient.clear()
      }
    }
  })
})

Такой механизм предотвращает ситуацию, когда:

  • access token истёк
  • backend уже считает сессию недействительной
  • но frontend продолжает показывать старые данные

Очистка persist-кеша

Наиболее опасная ошибка — сохранение чувствительных данных в localStorage.

Например:

persistQueryClient({
  queryClient,
  persister
})

По умолчанию persist-механизм может записывать весь кеш целиком.

Если туда попадут:

  • токены
  • профиль пользователя
  • банковские данные
  • приватные API-ответы

то информация останется в браузере даже после закрытия вкладки.


Исключение чувствительных query

Безопасный подход:

persistQueryClient({
  queryClient,
  persister,
  dehydrateOptions: {
    shouldDehydrateQuery: (query) => {
      return query.meta?.persist !== false
    }
  }
})

Теперь чувствительные запросы можно помечать:

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  meta: {
    persist: false
  }
})

Очистка localStorage при logout

Если используется persistence, очистки query cache недостаточно.

Необходимо удалить сохранённый кеш:

const logout = async () => {
  queryClient.clear()

  localStorage.removeItem('REACT_QUERY_OFFLINE_CACHE')
}

Иначе после перезагрузки страницы данные снова восстановятся.


Сокращение времени жизни кеша

Для чувствительных данных рекомендуется минимизировать время хранения.

Опасный пример:

useQuery({
  queryKey: ['bank-account'],
  queryFn: fetchBankAccount,
  staleTime: 1000 * 60 * 30,
  gcTime: 1000 * 60 * 60
})

Здесь данные могут храниться часами.

Безопаснее:

useQuery({
  queryKey: ['bank-account'],
  queryFn: fetchBankAccount,
  staleTime: 0,
  gcTime: 1000 * 30
})

Особенно важно уменьшать:

  • gcTime
  • staleTime
  • offline persistence

Отключение кеширования полностью

Иногда чувствительные данные вообще нельзя хранить.

Например:

  • OTP-коды
  • одноразовые ссылки
  • платёжные подтверждения
  • временные access token

В таких случаях:

useQuery({
  queryKey: ['otp'],
  queryFn: fetchOtp,
  staleTime: 0,
  gcTime: 0
})

или даже:

useQuery({
  queryKey: ['otp'],
  queryFn: fetchOtp,
  networkMode: 'always'
})

Очистка данных при смене пользователя

Особенно опасен сценарий shared device.

Например:

  1. Пользователь A вошёл в систему
  2. Данные сохранились в кеше
  3. Пользователь A вышел
  4. Пользователь B вошёл
  5. Приложение показывает старые данные

Причина обычно выглядит так:

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile
})

Ключ query одинаков для всех пользователей.


Привязка query к user id

Безопаснее:

useQuery({
  queryKey: ['profile', user.id],
  queryFn: fetchProfile
})

Теперь кеш разделяется между пользователями.


Очистка при смене аккаунта

Дополнительно:

authStore.subscribe((user) => {
  if (!user) {
    queryClient.clear()
  }
})

Очистка mutation cache

Многие забывают, что TanStack Query хранит не только query, но и mutation.

Например:

const mutation = useMutation({
  mutationFn: updatePassword
})

Mutation cache может содержать:

  • request payload
  • response data
  • ошибки
  • stack trace

После logout рекомендуется:

queryClient.getMutationCache().clear()

Хотя:

queryClient.clear()

делает это автоматически.


Удаление optimistic updates

Optimistic update может временно содержать чувствительные данные.

Пример:

queryClient.setQueryData(
  ['profile'],
  updatedProfile
)

Если запрос завершится ошибкой, временные данные могут остаться в кеше.

Безопасный rollback:

onError: (error, variables, context) => {
  queryClient.setQueryData(
    ['profile'],
    context.previousProfile
  )
}

После logout:

queryClient.cancelQueries()
queryClient.clear()

Очистка inactive queries

Даже неактивные query могут хранить данные.

Проверка:

queryClient.getQueryCache().getAll()

Удаление:

queryClient.removeQueries({
  type: 'inactive'
})

Защита Devtools

TanStack Query Devtools показывают содержимое кеша.

В production это опасно.

Неправильно:

<ReactQueryDevtools />

Правильно:

{process.env.NODE_ENV === 'development' && (
  <ReactQueryDevtools />
)}

Иначе любой пользователь сможет увидеть:

  • query data
  • headers
  • ошибки
  • внутренние структуры

Очистка при закрытии вкладки

Дополнительная мера безопасности:

window.addEventListener('beforeunload', () => {
  queryClient.clear()
})

Особенно полезно:

  • в банковских приложениях
  • административных системах
  • корпоративных панелях

Автоматическая очистка по таймеру

Некоторые системы требуют auto logout.

Пример:

let inactivityTimer

const resetTimer = () => {
  clearTimeout(inactivityTimer)

  inactivityTimer = setTimeout(() => {
    queryClient.clear()
  }, 1000 * 60 * 15)
}

window.addEventListener('mousemove', resetTimer)
window.addEventListener('keydown', resetTimer)

Очистка кеша между вкладками

Если logout произошёл в одной вкладке, остальные вкладки также должны удалить данные.

Пример через storage event:

window.addEventListener('storage', (event) => {
  if (event.key === 'logout') {
    queryClient.clear()
  }
})

Logout:

localStorage.setItem('logout', Date.now())

Исключение токенов из query cache

Критическая ошибка — хранение access token внутри query.

Плохо:

useQuery({
  queryKey: ['auth'],
  queryFn: async () => {
    return {
      token,
      user
    }
  }
})

Токены не должны находиться:

  • в query data
  • в mutation data
  • в persisted cache

Лучше:

  • хранить access token в memory store
  • использовать httpOnly cookie
  • изолировать auth layer

Санитизация серверных ответов

Backend иногда возвращает лишние данные.

Например:

{
  "id": 1,
  "email": "user@test.com",
  "passwordHash": "...",
  "internalFlags": [],
  "token": "..."
}

Перед сохранением:

useQuery({
  queryKey: ['profile'],
  queryFn: async () => {
    const data = await fetchProfile()

    return {
      id: data.id,
      email: data.email
    }
  }
})

Очистка Infinite Query

Infinite Query хранит массив страниц.

Например:

data.pages

Если страницы содержат чувствительные данные, очистка становится особенно важной:

queryClient.removeQueries({
  queryKey: ['transactions']
})

Безопасность SSR и hydration

При SSR TanStack Query сериализует кеш:

dehydrate(queryClient)

Если не фильтровать данные, чувствительная информация попадёт в HTML страницы.

Безопасная фильтрация:

dehydrate(queryClient, {
  shouldDehydrateQuery: (query) => {
    return query.meta?.sensitive !== true
  }
})

Чувствительный query:

useQuery({
  queryKey: ['private'],
  queryFn: fetchPrivateData,
  meta: {
    sensitive: true
  }
})

Использование select для минимизации данных

Иногда сервер возвращает большой объект, но UI использует только часть.

Вместо хранения полного ответа:

useQuery({
  queryKey: ['user'],
  queryFn: fetchUser
})

лучше:

useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  select: (data) => ({
    id: data.id,
    name: data.name
  })
})

Это уменьшает объём чувствительной информации в кеше.


Безопасная архитектура хранения данных

Практика безопасного использования TanStack Query обычно строится по следующим принципам:

Тип данных Стратегия
Public API Обычное кеширование
User profile Короткий gcTime
JWT token Не хранить в query
Банковские данные Без persistence
OTP Без кеширования
Admin data Полная очистка при logout
SSR private data Исключать из dehydrate
Infinite sensitive lists Агрессивная очистка

Комбинированная стратегия безопасности

На практике защита строится сразу из нескольких механизмов:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 0,
      gcTime: 1000 * 60,
      retry: false
    }
  }
})

const logout = async () => {
  queryClient.cancelQueries()
  queryClient.clear()

  localStorage.removeItem(
    'REACT_QUERY_OFFLINE_CACHE'
  )
}

Чувствительные query:

useQuery({
  queryKey: ['private', user.id],
  queryFn: fetchPrivateData,
  gcTime: 0,
  meta: {
    persist: false,
    sensitive: true
  }
})

Такая схема:

  • ограничивает время хранения данных
  • запрещает persistence
  • разделяет кеш пользователей
  • очищает данные при logout
  • предотвращает восстановление приватной информации
  • снижает риск XSS-последствий
  • уменьшает объём данных в памяти браузера