181 874 встречи нараспашку: разбор дыры в tl;dv и чек-лист по Firestore Rules

Есть класс уязвимостей, у которого нет ни CVE, ни эксплойта, ни патча. Просто где-то в проекте не написали пять строк. История с сервисом расшифровки встреч tl;dv — образцовый экземпляр, и заодно повод посмотреть на собственный Firebase-проект.

Ниже — хронология по датам, разбор механики и то, что стоит проверить у себя.

28 января: отчёт уходит в компанию

Исследователь под ником BobDaHacker пишет сооснователю tl;dv Рафаэлю Альштадту в LinkedIn и, отдельно, техническому директору. Суть отчёта: коллекция meetings в Firestore отдаётся любому аутентифицированному пользователю сервиса, без разделения по клиентам.

29–30 января Альштадт отвечает, что CTO свяжется немедленно. CTO не связывается.

Что именно было открыто

По публикации исследователя, доступными оказались:

  • 181 874 записи о встречах;
  • 84 312 уникальных пользователей;
  • 35 003 почтовых домена, включая государственные из 23 стран;
  • около 1 000 встреч в статусе recording — то есть идущих прямо сейчас;
  • более 1 000 публично доступных записей, где вскрылись 715 адресов приглашённых из 228 доменов.

В полях записи лежали почта создателя встречи, платформа конференции, метки времени, статус записи и — это ключевое — идентификатор конференции. То есть строка, по которой в комнату Google Meet или Teams можно просто зайти. Исследователь это проверил: подключился к двум чужим звонкам, включая совещание Министерства образования Малайзии на 157+ участников.

Error:

Отдельно стоит зафиксировать сам метод проверки. Подключение к чужому идущему звонку — это уже не «чтение открытых данных», а несанкционированный доступ, независимо от того, насколько дырявой была система. Этичный research останавливается на доказательстве возможности, а не на её реализации. Не повторяйте это ни на чьей инфраструктуре, включая свою SaaS-подписку.

Как это работало технически

Механика на удивление скучная, и в этом всё дело.

Клиент tl;dv логинится, получает JWT, обменивает его на Firebase-токен через gw.tldv.io/v1/users/firebase/token — и дальше ходит напрямую в Firestore, в проект lmi-store. Никакого промежуточного API, который проверял бы «а твоя ли это встреча», в этой цепочке нет вообще.

Именно в этом главная ловушка Firebase-архитектуры. В классической трёхзвенке между клиентом и базой стоит ваш сервер, и «забыть проверить tenant_id» надо в конкретном обработчике. В Firebase сервера посередине нет: Security Rules — это и есть весь бэкенд по части авторизации. Пропущенное правило на одной коллекции означает, что коллекция открыта целиком.

Important:

request.auth != null — это проверка «человек зарегистрировался», а не «человек имеет право на эту запись». В продукте, где регистрация открыта всем желающим, первое условие не значит ровно ничего.

14 февраля — 22 июля: тишина

Дальше в хронологии почти нет событий, и это самая неприятная её часть.

Дата Что произошло
28 января 2026 первый отчёт, LinkedIn + письмо CTO
29–30 января обещание, что CTO ответит немедленно
14–19 февраля повторные напоминания, ответа нет
6 марта исследователь перепроверяет — дыра на месте
22 июля спустя почти полгода дыра всё ещё на месте
4 августа материал выходит в Dark Reading, реакции по-прежнему нет

На странице безопасности tl;dv заявлено время реакции на сообщения об уязвимостях — 24 часа. Публичного заявления компании по существу на момент подготовки этого текста найти не удалось.

Warning:

Вторая находка того же исследования: внутреннее приложение с прогнозами на чемпионат мира на поддомене worldcup.tldv.io отдавало данные сотрудников через эндпоинт /api/entities/* вообще без авторизации. Конкретное число раскрытых записей в пересказах расходится, поэтому цифру здесь не привожу — важен сам паттерн: внутренняя игрушка на том же домене, что и продакшен, живёт по правилам «да кто её найдёт».

Что проверить в своём проекте сегодня

Если у вас есть хоть что-то на Firebase — вот минимальный прогон.

1. Найдите коллекции без правил. Правило по умолчанию в «тестовом режиме» выглядит так и живёт ровно 30 дней, после чего часто заменяется на что-нибудь «временное»:

allow read, write: if request.time < timestamp.date(2026, 9, 1);

Ищите в firestore.rules любые if request.auth != null без дополнительных условий — это и есть уязвимая форма.

2. Привяжите доступ к владельцу, а не к факту логина.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    match /meetings/{meetingId} {
      allow get: if request.auth != null
                 && resource.data.ownerId == request.auth.uid;

      allow list: if request.auth != null
                  && request.query.limit <= 100;

      allow create: if request.auth != null
                    && request.resource.data.ownerId == request.auth.uid;

      allow update, delete: if request.auth != null
                            && resource.data.ownerId == request.auth.uid;
    }
  }
}

3. Помните, что правила — не фильтр. Документация Firebase формулирует это прямо: «Вы не можете написать запрос на все документы коллекции и ожидать, что Cloud Firestore вернёт только те документы, к которым у клиента есть доступ». Если правило требует совпадения ownerId, то и сам клиентский запрос обязан содержать соответствующий where — иначе он просто упадёт. Многих это удивляет, и в этот момент рождается соблазн «пока ослаблю правило, потом верну». Не возвращают.

Success:

Регламент, который реально работает:

  • правила лежат в репозитории и катятся через firebase deploy --only firestore:rules, а не правятся в консоли руками;
  • на каждую коллекцию есть тест в firebase emulators:start — проверяющий не только «свой видит своё», но и «чужой не видит чужого»;
  • любой поддомен продакшен-зоны считается продакшеном, включая внутренние поделки;
  • в форме приёма сообщений об уязвимостях указан адрес, который читает не только маркетинг.
Security:

И потребительский вывод, который касается всех: ИИ-ноутейкер в вашем календаре — это внешняя компания, у которой лежат аудио, транскрипты и список участников всех ваших встреч. Модель угроз тут не «сломают шифрование», а «в одной коллекции забудут одну строчку». Если через встречу проходят персональные данные, коммерческая тайна или NDA, решение о подключении такого сервиса — это решение об обработке данных третьей стороной, со всеми вытекающими вопросами к договору и юрисдикции.

Question:

Кто разбирал свои Firestore Rules позже, чем «когда написал первую версию приложения»? И отдельный вопрос к тем, кто отправлял отчёты об уязвимостях в SaaS: сколько раз вам вообще отвечали?

Источники