АрхитектураАрхитектура безопасностиИнженер по безопасности приложений

Сервис кэширует ответы API по идентификатору ресурса без идентификатора арендатора. Какую архитектурную оши...

Сервис кэширует ответы API по идентификатору ресурса без идентификатора арендатора. Какую архитектурную ошибку это создаёт?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

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

Исторический контекст

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

В многопользовательских и мультитенантных системах один и тот же технический идентификатор может существовать в разных пространствах владения. Кэш, который не учитывает эту границу, сохраняет производительную оптимизацию, но разрушает изоляцию данных.

Постановка проблемы

Пусть у арендатора A есть ресурс с идентификатором 42, а у арендатора B — другой ресурс с тем же идентификатором. Если ключом кэша служит только значение 42, оба запроса обращаются к одной записи кэша.

Если сначала обращается арендатор A, кэш сохраняет его ответ. Запрос арендатора B может получить этот ответ напрямую, ещё до выполнения авторизации в сервисе-владельце данных. В результате возникает межтенантная утечка данных, которая может включать персональные сведения, коммерческую информацию или документы.

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

Подробное решение

Ключ кэша должен включать все параметры, влияющие на содержимое и доступность ответа. Для ресурса, принадлежащего арендатору, это обычно комбинация вида тип операции, версия представления, идентификатор арендатора, идентификатор ресурса.

Однако одного идентификатора арендатора недостаточно, если разные пользователи внутри одного арендатора имеют разные права. Тогда нужно учитывать разрешения или иной безопасный вариант authorization-контекста. Сохранять в ключе весь токен обычно не следует: это увеличивает размер ключа, ухудшает попадание в кэш и может привести к обращению с секретом как с обычными данными.

Практически применяют несколько стратегий:

  • кэшируют только общие данные, одинаковые для всех пользователей;
  • разделяют кэш по арендатору и дополнительно проверяют пользовательские права при каждом чтении;
  • кэшируют результат авторизации с коротким сроком жизни и корректной инвалидацией;
  • полностью отключают общий кэш для чувствительных или строго пользовательских ответов;
  • помещают проверку принадлежности ресурса арендатору в слой, который выполняется до чтения из кэша.

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

Нужно учитывать инвалидацию. После изменения владельца, состава участников или прав доступа ранее сохранённый ответ может стать недопустимым. Короткий TTL уменьшает окно риска, но не заменяет корректную инвалидацию: в течение TTL утечка всё ещё возможна.

Нужно также защищать сам механизм формирования ключа. Нормализация идентификаторов, фиксированный формат и запрет неоднозначных представлений предотвращают ситуации, когда разные логические контексты неожиданно сводятся к одному ключу. Полезны негативные тесты: один и тот же идентификатор в разных арендаторах должен приводить к разным ключам и разным результатам.

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

Ситуация из практики

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

Рассматривались три варианта. Первый — оставить ключ неизменным и полагаться на проверку прав в приложении; это не работает для попадания в кэш, потому что приложение могло вернуть результат до обращения к слою авторизации. Второй — полностью отключить кэш; это устраняет риск, но резко увеличивает нагрузку на базу. Третий — включить идентификатор арендатора в ключ, а проверку пользовательского доступа оставить обязательной на каждом запросе.

Был выбран третий вариант. Ключ стал зависеть от арендатора, а кэширование применялось только после проверки разрешения и не использовалось для ответов, персонализированных по пользователю. Для изменения ролей и состава участников добавили инвалидацию связанных записей.

Результат: данные разных арендаторов перестали пересекаться, а большая часть производительности сохранилась. При этом команда отдельно проверила случай одинаковых идентификаторов ресурсов, отзыв доступа и последовательность запросов после изменения прав.

Что кандидаты часто упускают

1. Достаточно ли добавить идентификатор арендатора в ключ кэша?

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

Нужно определить, является ли ответ одинаковым для всех пользователей арендатора. Если нет, применяют пользовательский или permission-aware контекст, либо выполняют авторизацию перед каждой выдачей и кэшируют только данные, не зависящие от субъекта. Важно не подменять проверку права принадлежностью к арендатору.

2. Можно ли считать короткий TTL достаточной защитой?

Нет. TTL ограничивает продолжительность существования ошибочного ответа, но не предотвращает его первое раскрытие. Даже несколько секунд могут быть критичны для персональных данных или секретов.

Короткий TTL полезен как дополнительное ограничение ущерба. Основная защита — корректное разделение ключей, обязательная авторизация и инвалидация при изменении прав или владельца ресурса.

3. Как обнаружить такую ошибку до эксплуатации?

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

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