Back

Что никогда не следует кешировать

Что никогда не следует кешировать

Кеширование — один из лучших инструментов оптимизации производительности фронтенда. При правильном применении оно устраняет избыточные сетевые запросы, снижает нагрузку на сервер и делает приложение отзывчивым. При неправильном — приводит к утечке личных данных, отдаёт устаревший аутентифицированный контент или блокирует пользователей в сломанных состояниях страниц, из которых невозможно выйти.

Правило проще, чем его описывают в большинстве статей: статические версионированные публичные ресурсы почти всегда безопасно кешировать агрессивно. Всё остальное по умолчанию следует считать небезопасным для кеширования.

Прежде чем перейти к тому, что не следует кешировать, важно чётко понять, где именно происходит кеширование, поскольку эти уровни ведут себя принципиально по-разному.

Ключевые выводы

  • Уровни кеширования (HTTP, CDN, Service Worker, bfcache, Web Storage) имеют различные области видимости и характеристики безопасности; их взаимозаменяемое использование порождает реальные ошибки.
  • no-store полностью запрещает сохранение ответа, тогда как no-cache лишь требует ревалидации перед повторным использованием.
  • Аутентифицированные или пользовательские ответы должны использовать Cache-Control: private, no-store, чтобы предотвратить утечку данных между пользователями через общие кеши.
  • Никогда не храните JWT, refresh-токены или API-ключи в localStorage или IndexedDB; отдавайте предпочтение HttpOnly-кукам.
  • По умолчанию используйте no-store везде, где устаревшая или общая копия может стать причиной проблем с безопасностью или корректностью данных.

Уровни кеширования не взаимозаменяемы

Большинство ошибок кеширования начинается с того, что следующие механизмы воспринимаются как одно и то же:

  • HTTP/браузерный кеш — управляется заголовками Cache-Control согласно RFC 9111. Является общим для всех вкладок и сохраняется между сессиями.
  • CDN/общий кеш — располагается между пользователями и исходным сервером. Сервисы наподобие Cloudflare CDN кешируют ответы для всех пользователей, а не для одного конкретного.
  • Service Worker Cache API — программируемый кеш, полностью управляемый через JavaScript. Сохраняется до явной очистки.
  • bfcache (кеш навигации назад/вперёд) — хранящийся в памяти браузера снимок полной страницы, используемый при навигации назад. Отдельный от HTTP-кеша механизм.
  • localStorage / sessionStorage — хранилище типа «ключ — значение». Не имеет встроенного управления сроком жизни, полностью доступно из JavaScript.
  • IndexedDB — структурированное персистентное хранилище. Имеет такую же поверхность атаки через XSS, что и localStorage.

Каждый из этих механизмов обладает различными характеристиками персистентности, области видимости и безопасности. Их смешение порождает реальные ошибки.

Краткое пояснение семантики Cache-Control

Это различие часто трактуется неверно, поэтому стоит изложить его чётко:

  • no-store означает, что кеш не должен сохранять ответ вообще.
  • no-cache не означает «не кешировать». Это означает, что перед повторным использованием кешированная копия должна пройти ревалидацию на сервере.

Если вы хотите, чтобы ответ нигде не сохранялся, используйте no-store. Если вам нужна гарантия актуальности при каждом обращении, но при этом вы хотите сохранить эффективность условных запросов (ETag, 304), используйте no-cache.

Что никогда не следует кешировать

Аутентифицированные API-ответы и пользовательский HTML

Любой ответ, зависящий от идентификатора пользователя, — HTML дашборда, страницы аккаунта, API-ответы с данными профиля — не должен кешироваться в общем кеше. Стандартный подход:

Cache-Control: private, no-store

no-store запрещает обычным HTTP-кешам сохранять ответ, а private явно указывает общим кешам, таким как CDN, не кешировать его для нескольких пользователей.

Без директивы private CDN-сервис наподобие Cloudflare может закешировать ответ, возвращённый для пользователя A, и отдать его пользователю B. Подобные инциденты с утечкой данных уже происходили в реальной практике.

Всё, что находится в Service Worker и требует аутентификации

Service Worker Cache API фактически представляет собой программируемый сетевой прокси. Service Worker перехватывает каждый fetch-запрос в своей области видимости. Если вы бездумно кешируете в нём аутентифицированные API-ответы или пользовательский HTML, эти данные сохраняются в хранилище Cache API — потенциально между сессиями и доступны service worker’у вне зависимости от состояния авторизации.

// ❌ Так делать не нужно
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request).then(response => {
      return caches.open('v1').then(cache => {
        cache.put(event.request, response.clone());
        return response;
      });
    })
  );
});

Этот паттерн кеширует всё без разбора. Явно исключайте аутентифицированные маршруты:

// ✅ Кешируем только публичные версионированные ресурсы
const CACHEABLE = ['/shell.html', '/app-abc123.js', '/styles-f9c.css'];

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  if (url.origin !== self.location.origin) return; // игнорируем cross-origin запросы
  if (!CACHEABLE.includes(url.pathname)) return;   // пропускаем запрос в сеть
  event.respondWith(
    caches.match(event.request).then(r => r || fetch(event.request))
  );
});

Секреты в localStorage или IndexedDB

OWASP HTML5 Security Cheat Sheet однозначно указывает: хранение JWT, refresh-токенов, API-ключей или токенов сброса пароля в localStorage или IndexedDB сопряжено с риском, поскольку любая XSS-уязвимость на вашем домене позволяет их прочитать. Эти механизмы хранения не имеют встроенного управления сроком жизни, аналога атрибута HttpOnly и изоляции от внедрённых скриптов.

По возможности используйте куки с атрибутами HttpOnly, Secure, SameSite для хранения токенов. Если вы вынуждены использовать localStorage, осознавайте, что принимаете на себя соответствующий риск.

Пользовательские ответы на уровне CDN

Распространённая ошибка при работе с Cloudflare CDN: отсутствие директивы private в ответах, содержащих пользовательские данные, или некорректная настройка заголовка Vary.

# ❌ Отсутствует private — CDN может закешировать это для всех пользователей
Cache-Control: max-age=300
Content-Type: application/json

# ✅ Правильный вариант
Cache-Control: private, no-store

Если ваш ответ правомерно зависит от заголовка запроса (например, Accept-Language или Authorization), необходимо включить корректный заголовок Vary. Без него CDN может отдать закешированный ответ на французском языке англоязычному пользователю или, что хуже, закешированный аутентифицированный ответ — неаутентифицированному пользователю.

Также обратите внимание на отравление кеша через неучтённые параметры: если ваш CDN кеширует по URL, а приложение считывает непроверенный параметр запроса или заголовок для формирования ответа, злоумышленник может отравить кеш с помощью специально сформированного запроса.

Страницы, которые не должны восстанавливаться из bfcache

В последних версиях браузеры на базе Chromium стали активнее использовать bfcache даже для страниц, отправляющих Cache-Control: no-store. Это поведение зависит от конкретного браузера и не является универсальным для Chrome, Firefox и Safari. По данным caniuse, bfcache поддерживается большинством современных браузеров, однако взаимодействие с директивой no-store различается в зависимости от движка и версии браузера.

Если у вас есть страницы, для которых восстановление устаревшего снимка из памяти действительно небезопасно, — например, страница подтверждения разового платежа или страница с чувствительным к сессии состоянием, — используйте событие pageshow для обнаружения восстановления из bfcache и последующей перезагрузки или ревалидации:

window.addEventListener('pageshow', event => {
  if (event.persisted) {
    // Страница восстановлена из bfcache
    window.location.reload();
  }
});

Не рассчитывайте на то, что no-store универсально отключает bfcache во всех браузерах.

Базовая ментальная модель

Если вы не уверены, стоит ли что-то кешировать, задайте себе вопрос: может ли отдача устаревшей или общей копии этого ресурса стать причиной проблем с безопасностью или корректностью данных? Если да — по умолчанию используйте Cache-Control: no-store и двигайтесь от этой точки.

Статические ресурсы с хешированными URL — JavaScript-бандлы, CSS-файлы, изображения — можно безопасно кешировать на год с помощью max-age=31536000, immutable. Всё, что связано с пользовательской сессией, состоянием аутентификации или чувствительными операциями, — нет.

Заключение

Агрессивное кеширование — это полезная функция. Случайное кеширование неподходящих данных — это уязвимость. Профессионализм состоит в том, чтобы знать, на какой уровень вы воздействуете, что именно гарантирует каждая директива Cache-Control и какие категории данных никогда не должны выходить из-под жёсткого контроля исходного сервера. По умолчанию считайте всё, что связано с пользователем, сессией или секретными данными, некешируемым — и резервируйте агрессивное кеширование для статических версионированных ресурсов, для которых оно и было создано.

Часто задаваемые вопросы

Это возможно, но не рекомендуется. Любая XSS-уязвимость на вашем домене даёт злоумышленнику полный доступ на чтение localStorage, что означает возможность кражи токена и имперсонации пользователя. Куки с атрибутами HttpOnly, Secure, SameSite безопаснее, поскольку JavaScript не может их прочитать напрямую. Если вы вынуждены использовать localStorage, вы принимаете на себя риск XSS и должны уделить особое внимание CSP и санитизации входных данных.

no-store запрещает обычным HTTP-кешам сохранять ответ для повторного использования. no-cache разрешает сохранение ответа, но требует ревалидации на исходном сервере перед каждым повторным использованием — как правило, через условные запросы с ETag или Last-Modified. Используйте no-store для чувствительных данных и no-cache, когда вам нужны гарантии актуальности, но при этом важно сохранить преимущества ответов 304 Not Modified.

Как правило, это происходит из-за отсутствия директивы private или некорректного заголовка Vary. Без private общий кеш считает ответ кешируемым для всех. Без Vary по заголовкам Authorization или Cookie CDN игнорирует эти заголовки при формировании ключей кеша и может отдать ответ пользователя A пользователю B. Всегда отправляйте Cache-Control: private, no-store для пользовательских ответов.

Не существует надёжного кросс-браузерного заголовка, отключающего bfcache. Cache-Control: no-store работает в некоторых браузерах, но непоследовательно. Надёжный подход — прослушивать событие pageshow и проверять event.persisted. Если значение равно true, страница была восстановлена из bfcache, и вы можете принудительно перезагрузить её или повторно запросить чувствительное состояние. Это особенно важно для страниц подтверждения платежей и страниц после выхода из системы.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay