Site icon AppTractor

Как ошибка двухлетней давности привела к тому, что мое Flutter-приложение получило счет за ИИ на €3167

В апреле 2026 года моё Flutter-приложение было представлено на презентации Google Cloud Next’26 для разработчиков в Лас-Вегасе. Три недели спустя я обнаружил, что тот же проект заблокирован за злоупотребление, а за ночь с меня списали 3167 евро за использование Gemini Developer API.

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

Это история о том, как моя конфигурация Firebase привела к утечке данных.

1. Пет-проект, который незаметно вырос

Я создал Finnish It — приложение на Flutter для изучения финского языка, призванное помочь пользователям сдать экзамен на получение финского гражданства (YKI). Я разрабатывал его в свободное время, по ночам и в выходные, изначально для себя (более подробная история — в моем посте на LinkedIn о выступлении).

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

Все изменилось во время пасхальных каникул. Firebase представил интеграцию AI Logic с Gemini, и я провел праздники, внедряя свою первую функцию на основе ИИ. Это было похоже на волшебство.

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

Год спустя, в апреле 2026 года:

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

Вместо этого мне следовало подумать о чем-то более простом и менее лестном: в тот момент Finnish It перестал быть просто личным проектом. У него появились платные подписчики. У него появился ежемесячный доход. У него появился публичный профиль. Реальные люди начали от него зависеть. Мне следовало начать относиться к нему как к небольшому продукту, за который я несу ответственность.

Я не до конца это осознал. Мы редко это делаем.

Несколько недель спустя урок был усвоенна собственном горьком опыте.

2. Несколько часов, 3167 евро

9 мая я проснулся и обнаружил электронное письмо от Google Cloud:

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

Проект был заморожен. Приложение не работало. Панель управления платежами рассказала остальную историю:

Бот получил рабочие учетные данные и расходовал токены так быстро, как позволял API.

3. Первая теория: жёстко прописанный ключ (неверно)

Первая реакция была очевидной и неверной: «В исходном коде должен быть жёстко прописанный ключ».

Я проверил код. Репозиторий всегда был приватным.

Я вернулся к панели API-ключей AI Studio и вывел все ключи, когда-либо сгенерированные проектом:

Третий ключ пролежал там восемнадцать месяцев. Изначально он был автоматически сгенерирован для веб-версии приложения в firebase_options.dart, когда я настраивал Flutter Web при инициализации проекта — решение «на всякий случай, если я когда-нибудь выпущу веб-версию», которое я принял, а затем забыл.

Но значение ключа всегда находилось только в приватном репозитории, а веб-платформа была удалена из исходного кода более шести месяцев назад. Так как же он оказался в руках бота?

4. Реальная причина

Ответ оказался в функции Firebase Hosting, о которой большинство разработчиков никогда не слышали.

Зарезервированный URL

Если в вашем проекте Firebase зарегистрировано веб-приложение И имеется какое-либо развертывание Hosting для этого проекта, Firebase Hosting автоматически предоставляет доступ ко всей конфигурации вашего веб-SDK по зарезервированному URL:

https://<project-id>.firebaseapp.com/__/firebase/init.json

Ответ представляет собой обычный JSON. Никакой аутентификации. Никакой проверки реферера. Никакой проверки приложения. Только конфигурация.

Я подтвердил это одной командой:

$ curl https://<project-id>.web.app/__/firebase/init.json
{
  "apiKey": "AIzaSy…92fkQ",
  "projectId": "<project-id>",
  "authDomain": "<project-id>.firebaseapp.com",
  ...
}

Это и была утечка.

Как страница, созданная только с помощью CSS, привела к утечке ключа API

Развернутая мной страница представляла собой статическую страницу на основе CSS. Никакого Firebase JS SDK. Никаких ссылок на ключи. Ничто на самой странице не имело отношения к этому эндпоинту.

Но не имело значения, что было на странице. Важно было то, что для проекта вообще был активен хостинг. Зарезервированный URL — это функция уровня проекта, а не уровня страницы. Как только в проекте был развернут какой-либо хостинг, конфигурация SDK начала предоставлять доступ — кому угодно, где угодно, без аутентификации.

У ботов, которые парсят *.web.app/__/firebase/init.json по всему пространству имен Firebase, были месяцы возможностей. В конце концов, один из них заметил мой ключ.

Ключ не имел ограничений, поэтому бот мог использовать его для любого API Google, который был включен в проекте, — включая Gemini Developer API.

Конфигурация Firebase

Вот как конфигурация Firebase оказалась вовлечена в утечку:

1. Автоматическое создание ключа API

Настройка целевого объекта Flutter Web в flutterfire configure автоматически сгенерировала ключ для браузера. Я никогда не публиковал целевой объект Web своего приложения и удалил web config из firebase_options.dart полгода назад — но сам ключ API оставался активным в Cloud Console в течение двух полных лет, потому что ключи хранятся в облаке, а не в исходном коде.

2. Отсутствие ограничений на момент создания

Когда этот проект был создан в 2024 году, ключ был сгенерирован без ограничений API и без ограничений HTTP-рефереров. Любой источник в интернете мог вызывать любой сервис в проекте. Было ли это значением по умолчанию на тот момент, ранней ошибкой или каким-то образом решением, принятым вручную, я уже забыл, честно говоря, сказать не могу. Журнал аудита не охватывает два года, но это значение по умолчанию было исправлено. В разделе 7 описана хронология событий и ответ команды Firebase.

3. Автоматическая публикация.

После того, как в проекте появился хостинг, зарезервированная конечная точка __/firebase/init.json предоставляла неограниченный ключ любому, кто знал шаблон URL-адреса.

5. Индустрия ожидала этого

После того, как я поделился своей историей на X, Франк ван Пуффелен указал мне на вебинар Truffle Security, опубликованный несколькими месяцами ранее.

Вебинар называется «Ключи API Google не являются секретными. Но Gemini изменил правила»:

На протяжении большей части истории Google ключи API не рассматривались как секреты. В собственной документации Google они позиционировались как идентификаторы, а не как учетные данные — их можно безопасно встраивать в мобильные бинарные файлы, безопасно включать в пакеты JavaScript, безопасно добавлять в firebase_options.dart и регистрировать в репозитории. Логика заключалась в том, что ограничения (ограничения API, ограничения рефереров, ограничения приложений) были настоящей защитой. Если ключ утекал, худшим сценарием обычно было недорогое, ограниченное по скорости злоупотребление общедоступным API данных. Неприятно, но восстанавливаемо.

Как упоминалось на вебинаре, Gemini изменил экономику: «Когда Gemini API включен в проекте, те же самые открытые ключи могут незаметно получить доступ к закрытым данным Gemini без предупреждения». Те же самые строки, которые нам годами говорили, что можно поставлть в проектах, сегодня используются в качестве учетных данных для API, за каждый вызов которого взимается реальная плата.

Сканеры Truffle обнаружили почти 3000 открытых ключей Google API в сети — в том числе и в собственной инфраструктуре Google. Многие из них получили доступ к Gemini через ту же самую модель ограничения доступа к проекту, которая меня зацепила: как только API генеративного языка включен в проекте, любой неограниченный ключ в этом проекте может вызывать его, независимо от того, для какого сервиса этот ключ был первоначально создан. Злоумышленнику не нужно искать «ключ Gemini». Ему нужно просто найти ключ.

В этом и заключается сдвиг. Со стороны злоумышленника сканеры, которые изначально были настроены на атаки с целью получения бесплатных кредитов, теперь находят ключи, которые генерируют платные результаты ИИ-вычислений за каждый запрос. С моей стороны, учетные данные, которым было восемнадцать месяцев и которые хранились в firebase_options.dart еще до появления firebase_ai, в одночасье превратились в работающий генератор расходов.

С тех пор эта ситуация получила широкое освещение в технологической прессе. 13 мая 2026 года — через пять дней после моего инцидента — The Register опубликовал статью о борьбе пользователей Google за возмещение средств из-за резкого роста счетов за несанкционированное использование API. Два из упомянутых случаев похожи на мой:

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

6. Урок 1: выбирайте Enterprise API вместо Developer API

Это самый важный архитектурный урок, и я усвоил его только задним числом.

Когда вы начинаете использовать firebase_ai, в официальном руководстве по началу работы представлены два варианта подключения к Gemini: Gemini Developer API и Gemini Enterprise Agent Platform API (ранее Vertex AI). Оба используют одни и те же модели. В документации они выглядят взаимозаменяемыми. Но это не так.

Вот что на самом деле между ними отличается.

Gemini Developer API — generativelanguage.googleapis.com

Gemini Enterprise Agent Platform API — aiplatform.googleapis.com

Утечка ключа API для доступа к Gemini Developer API дает рабочий доступ. Тот же ключ для доступа к Gemini Enterprise Agent Platform AI возвращает ошибку 401 Unauthorized. Enterprise и требует токены bearer, полученные либо из закрытого ключа учетной записи службы (на стороне сервера), либо из токена Firebase Auth ID плюс токен App Check (на стороне клиента, через Firebase AI Logic). Ни один из этих вариантов не является статической строкой, которую злоумышленник может скомпилировать и использовать повторно.

Переключение осуществляется одной строкой в ​​SDK Flutter firebase_ai:

// Before — uses Gemini Developer API, accepts API keys
FirebaseAI.googleAI(appCheck: FirebaseAppCheck.instance)
// After - Gemini Enterprise Agent Platform AI (previouslu Vertex), no API keys
FirebaseAI.vertexAI(appCheck: FirebaseAppCheck.instance, location: 'global')

На конференции Google Cloud Next я спросил @Thevi_S и @MiguelRamosPM из команды Firebase, что лучше выбрать для моего приложения: Gemini Enterprise Agent Platform AI или Gemini Developer API. Они настоятельно рекомендовали Gemini Enterprise Agent Platform для приложений ИИ в производственной среде. После этого инцидента я точно понимаю, почему.

Предостережение: как Firebase уже обеспечивает безопасность Gemini Developer API

Было бы несправедливо создавать впечатление, что Gemini Developer API сам по себе небезопасен. При использовании через Firebase AI Logic — FirebaseAI.googleAI(appCheck: FirebaseAppCheck.instance) — он действительно безопасен. За этой одной строкой скрываются три вещи:

Так как же мой проект был скомпрометирован через Gemini Developer API?

Потому что утекший ключ не был ключом Firebase AI Logic. Это был совершенно другой ключ — автоматически сгенерированный ключ браузера Firebase Web SDK, изначально созданный для инициализации аутентификации/Firestore/Storage на стороне клиента, а не для Gemini вообще.

Ключи Google Cloud API ограничены проектами, а не сервисами. Как только Unrestricted ключ появляется где-либо в вашем проекте, он может вызывать любой API Google, включенный в этом проекте, — включая сервисы, для которых он никогда не предназначался. Ключ Web SDK в моем браузере может вызывать Gemini, потому что мое приложение использует Firebase AI Logic с API разработчика Gemini.

После того как ваши запросы к ИИ пройдут через платформу Enterprise Agent Platform, вы можете полностью отключить Generative Language API в консоли Firebase или через CLI:

gcloud services disable generativelanguage.googleapis.com --project <your-project>

7. Урок 2: применяйте принципа YAGNI к платформам Flutter

При создании нового проекта Flutter, CLI FlutterFire запрашивает, какие платформы следует настроить. Возникает соблазн отметить все пункты — Android, iOS, Web, macOS, Windows, Linux — на всякий случай, если они понадобятся позже.

Не делайте этого.

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

Я настроил Flutter Web на этапе инициализации проекта два года назад, потому что подумал: «Может быть, когда-нибудь я сделаю веб-версию». Я так и не сделал этого. Раздел web пролежал в firebase_options.dart около года. Автоматически сгенерированный ключ для браузера пролежал в Cloud Console без ограничений целых два года. Ничего из этого не использовалось. Ничего из этого не должно было существовать.

Безжалостно применяйте принцип YAGNI к платформам: генерируйте только то, что вы выпускаете сегодня. Вы всегда можете добавить платформу позже — команда flutterfire configure --platforms=web занимает 30 секунд, когда она вам действительно нужна. Стоимость добавления позже незначительна. Стоимость хранения учетных данных неиспользуемой платформы в вашем проекте в течение двух лет, по-видимому, составляет 3167 евро.

8. Урок 3: проверяйте каждый принадлежащий вам ключ

Самый быстрый способ увидеть все API-ключи, доступные через Gemini, во всех ваших проектах — это страница API-ключей Google AI Studio. Она показывает суффикс ключа, проект, к которому он принадлежит, дату создания и — что очень важно — помечен ли он как «Неограниченный».

Когда я просматривал свои собственные ключи после инцидента, я увидел этот баннер:

Посмотрите на дату еще раз. Бот атаковал мой проект 8 мая 2026 года. Крайний срок Google для вывода из эксплуатации «Неограниченных» ключей — 19 июня 2026 года — примерно через шесть недель. Я был, в буквальном смысле, последней группой разработчиков, пострадавших от этого значения по умолчанию, прежде чем платформа отключит его для всего проекта. Это действительно хорошая новость для всех, кто читает этот пост во второй половине 2026 года: конкретная ошибка, которая обошлась мне в 3167 евро, находится в процессе исправления.

Команда Firebase уже занимается её устранением

После того, как я публично поделился этой историей, Себа Гнагнарелла из команды Firebase связался со мной в течение нескольких часов. Суть этого обмена сообщениями:

Две вещи, которые команда Firebase уже сделала задолго до моего инцидента, заслуживают четкого упоминания:

Это реальное решение, реализованное с реальными усилиями, и команда заслуживает похвалы как за изменение настроек, так и за информационную кампанию.

Привычка к аудиту

Даже с учетом готовящегося общеплатформенного исправления, урок не меняется. Раз в квартал уделяйте десять минут проверке каждого принадлежащего вам ключа и задайте себе три вопроса:

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

9. Урок 4: жесткие ограничения, а не просто оповещения

Бюджеты Cloud Billing (документация) — это оповещения, а не лимиты. Они отправляют электронное письмо, когда вы превышаете пороговое значение. Они не останавливают расходы.

Всплеск в 3167 евро произошел за несколько часов, за ночь. У меня было настроено оповещение о превышении бюджета на 100 евро. Однако я спал, когда оно сработало. К тому времени, как я проснулся и прочитал письмо, счет уже в 30 раз превысил пороговое значение оповещения.

На самом деле нам нужно жесткое ограничение:

Модель мышления:

Для любого API, который может обойтись вам дороже, чем вы можете позволить себе потерять за одну неудачную ночь, вам нужен второй сигнал. Предупреждения о бюджете полезны в качестве дополнительного сигнала, но сами по себе их недостаточно — особенно для таких API, как Gemini.

Еще один важный момент, о котором сообщалось в упомянутой ранее статье Register: система уровней расходов Google может незаметно автоматически увеличивать ваш лимит. У одного разработчика, Исуру Фонсеки, был жесткий лимит в 250 долларов, и тем не менее утром он обнаружил списания на сумму 17 000 австралийских долларов — потому что его аккаунт автоматически перешел на более высокий уровень (с диапазоном лимитов от 20 000 до 100 000 долларов), как только были превышены внутренние пороговые значения Google для расходов за все время и возраста аккаунта. Если вы установили лимит расходов, проверьте свой текущий тарифный план в Cloud Console и убедитесь, что не включено автоматическое повышение лимита.

10. Заключение: операционная безопасность — это тоже архитектура

Инцидент на сумму 3167 евро урегулирован. Команда Trust & Safety восстановила проект после того, как я объяснил первопричину. Cloud Billing проверяет как непогашенный баланс, так и уже списанные 500 евро. Приложение снова работает через два дня, с более строгой архитектурой, чем раньше.

Существует разница между технической и операционной безопасностью проекта. Я правильно проделал архитектурную работу — Firebase AI Logic на стороне клиента, проверка приложений через Play Integrity и App Attest, Cloud Functions с Secret Manager на сервере, отсутствие ключа Gemini на любом устройстве. Согласно всем контрольным спискам «лучших практик безопасности Firebase», которые я читал, проект был надежным.

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

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

Эпоха ИИ работает в обе стороны

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

Мы переживаем самый захватывающий момент в разработке приложений, который я помню. Flutter и Firebase вместе — это поистине волшебная пара: одна кодовая база, шесть платформ, бэкенд, который масштабируется сам по себе, функции ИИ за одним вызовом SDK и настолько хороший опыт разработчика, что «fullstack solo developer» — это реальная должность в 2026 году. Я люблю этот стек. Я построил на нём Finnish It. Я буду продолжать его развивать.

Но та же волна ИИ, которая заставляет нас чувствовать себя волшебниками в IDE, делает ботов по другую сторону интернета умнее, чем когда-либо. Та же модель, которая помогает вам создать облачную функцию за тридцать секунд, поможет злоумышленнику создать сканер пространств имен Firebase за те же тридцать секунд.

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

Что же на самом деле делать в эти выходные?

Если у вас есть побочный проект, который тихо вырос из «побочного», потратьте час на то, чтобы:

  1. Переключить все клиентские вызовы Gemini с Gemini Developer API на Gemini Enterprise Agent Platform AI (одна строка: FirebaseAI.vertexAI(...))
  2. Удалить все платформы Flutter, которые вы не используете, и автоматически созданные ими ключи API
  3. Запустить curl https://<ваш-проект>.web.app/__/firebase/init.json и посмотреть, что там находится
  4. Провести аудит каждого ключа API в AI Studio, ограничить использование тех, которые вы сохраняете; удалить те, которые вы не используете
  5. Установить жесткий лимит расходов для каждого API, который может стоить реальных денег

Цель не в том, чтобы быть параноиком. Цель в том, чтобы настройки по умолчанию работали на вас, а не против вас.

Создавайте с удовольствием. Защищайте с бдительностью. Боты не на дремлют.

Продолжайте выпускать продукты.

Exit mobile version