FixVibe

// docs / baas security / supabase storage

Чек-лист безопасности хранилища Supabase: 22 пункта

Supabase Storage — это тонкая обёртка вокруг корзины, совместимой с S3, плюс та же модель безопасности на уровне строк, что и у базы данных. Это означает, что те же подводные камни RLS, которые влияют на таблицы, влияют на доступ к файлам — и несколько специфичных для хранилища, которые проявляются, когда ИИ-инструменты кодирования настраивают загрузки. Этот чек-лист — 22 пункта в пяти разделах: конфигурация корзины, политики RLS, проверка загрузки, подписанные URL и операционная гигиена. Каждый можно проверить менее чем за 15 минут.

Каждый пункт ниже является существенным. По основным механикам RLS см. Сканер Supabase RLS. По классу раскрытия ключей, смежному с хранилищем, см. Ключ сервисной роли Supabase, раскрытый в JavaScript.

Конфигурация корзины

Начните с правильных значений по умолчанию. Неправильно настроенная корзина утекает файлы независимо от того, правильный ли ваш RLS.

  1. По умолчанию делайте каждую корзину приватной. В панели Supabase → Storage → Buckets установите переключатель Public bucket в выключенное положение, если у вас нет явной причины (маркетинговые ресурсы, публичные аватары без PII). Публичные корзины обходят RLS для операций чтения — любой, кто знает имя корзины, может перечислять и загружать.
  2. Установите жёсткое ограничение размера файла на каждой корзине. Панель → Настройки корзины → Ограничение размера файла. 50 МБ — разумное значение по умолчанию для пользовательских загрузок; намеренно повышайте для случаев использования видео / больших файлов. Без ограничения одна вредоносная загрузка может исчерпать вашу квоту хранилища или ваш месячный трафик.
  3. Ограничьте разрешённые MIME-типы для каждой корзины. Список разрешённых MIME-типов — явный список разрешённых, а не запрещённых. image/jpeg, image/png, image/webp для корзин только с изображениями. Никогда не разрешайте text/html, application/javascript или image/svg+xml в корзине пользовательского контента — они выполняются в браузере при подаче через подписанный URL.
  4. Используйте одну корзину на тип контента, а не одну общую корзину. Настройки на корзину (размер, MIME-типы, политики RLS) — это уровень детализации, который у вас есть. Корзину user-avatars, корзину document-uploads и корзину public-assets легче заблокировать, чем одну смешанную корзину.
  5. Проверьте конфигурацию CORS, если фронтенд загружает. Если пользователи загружают напрямую из браузера в подписанный URL, CORS корзины должен включать ваше производственное происхождение. * приемлемо только для публичных корзин — никогда для корзин, содержащих PII пользователей.

Политики RLS на storage.objects

Supabase Storage хранит метаданные файлов в таблице storage.objects. RLS на этой таблице контролирует, кто может читать, загружать, обновлять или удалять файлы. Без RLS публичный/приватный флаг корзины — это ваша единственная защита.

  1. Подтвердите, что RLS включён на storage.objects. SELECT rowsecurity FROM pg_tables WHERE schemaname = 'storage' AND tablename = 'objects'; должен вернуть true. Supabase включает его по умолчанию в новых проектах; убедитесь, что он не был отключён.
  2. Напишите политику SELECT с ограничением через auth.uid() для приватных корзин. CREATE POLICY "users_read_own_files" ON storage.objects FOR SELECT USING (auth.uid()::text = (storage.foldername(name))[1]);. Соглашение состоит в том, чтобы хранить файлы по адресу [user-id]/[filename] и использовать storage.foldername() для извлечения владельца из пути.
  3. Напишите политику INSERT, которая обеспечивает то же соглашение о пути. CREATE POLICY "users_upload_own" ON storage.objects FOR INSERT WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);. Без WITH CHECK аутентифицированный пользователь может загружать в папку другого пользователя.
  4. Добавьте политики UPDATE и DELETE, если ваше приложение поддерживает редактирование или удаление файлов. Каждая команда нуждается в собственной политике. Пропуск DELETE означает, что аутентифицированные пользователи не могут удалять свои собственные файлы; пропуск UPDATE означает, что перезапись файлов тихо терпит неудачу.
  5. Протестируйте межпользовательский доступ в двух сеансах браузера. Войдите как Пользователь A, загрузите файл, скопируйте путь. Войдите как Пользователь B в другом браузере, попытайтесь получить файл через REST API. Ответ должен быть 403 или 404, никогда 200.
sql
-- Confirm RLS on storage.objects
SELECT rowsecurity
FROM   pg_tables
WHERE  schemaname = 'storage' AND tablename = 'objects';

-- SELECT policy: scope reads to the owning user's folder.
CREATE POLICY "users_read_own_files"
  ON storage.objects
  FOR SELECT
  USING (auth.uid()::text = (storage.foldername(name))[1]);

-- INSERT policy: enforce the [user-id]/[filename] path convention.
CREATE POLICY "users_upload_own"
  ON storage.objects
  FOR INSERT
  WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);

Проверка загрузки

Проверяйте каждую загрузку на стороне сервера, даже когда у корзины есть ограничения MIME и размера. ИИ-инструменты кодирования генерируют только клиентскую проверку по умолчанию; это ничего не защищает.

  1. Перепроверяйте MIME-тип на стороне сервера из фактических байтов файла, а не заголовка Content-Type. Используйте библиотеку вроде file-type (Node) или магический байт-сниффинг. Злоумышленник может заявить Content-Type: image/jpeg для файла, который на самом деле является полиглот HTML / JavaScript нагрузкой.
  2. Удаляйте метаданные EXIF из загруженных изображений. EXIF может содержать GPS-координаты, серийные номера устройств и временные метки. Используйте sharp с .withMetadata(false) или exif-parser для удаления перед сохранением.
  3. Отклоняйте SVG, содержащие теги script или обработчики onload. SVG — это XML — и многие ИИ-сгенерированные приложения разрешают загрузки SVG как «просто изображение». Используйте DOMPurify на стороне сервера или полностью отказывайтесь от загрузок SVG.
  4. Используйте детерминированные, непредугадываемые имена файлов. Не сохраняйте исходное имя файла. Используйте UUID или хеш содержимого файла. Исходные имена файлов утекают ("passport_scan_2024_01_15.jpg"), а предсказуемые имена позволяют перечисление.

Подписанные URL

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

  1. По умолчанию срок действия подписанного URL — 1 час или меньше. createSignedUrl(path, expiresIn) Supabase JS SDK принимает секунды. Никогда не используйте значения вроде 31536000 (один год) — URL становится постоянной полупубличной ссылкой.
  2. Никогда не храните подписанные URL в вашей базе данных. Генерируйте свежие на стороне сервера для каждого запроса. Сохранённый подписанный URL с годовым сроком действия, который утекает через дамп базы данных, даёт долгосрочный доступ.
  3. Регистрируйте генерацию подписанных URL, а не только загрузки файлов. Если позже вы подозреваете компрометацию, вам нужно знать, кто сгенерировал какой URL и когда. Регистрируйте auth.uid() + корзина + путь объекта + временная метка.
  4. Используйте опцию downloadAs при предоставлении файлов, загруженных пользователями. createSignedUrl(path, expiresIn, { download: '.jpg' }) принудительно устанавливает заголовок Content-Disposition: attachment, поэтому файл загружается, а не отображается — побеждает класс выполнения HTML / SVG / HTML в PDF.

Операционная гигиена

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

  1. Проверяйте корзины ежеквартально. Панель → Storage → Buckets. Подтвердите, что состояние public/private и списки MIME-типов соответствуют ожиданиям приложения. Корзины, созданные «временно», становятся постоянными, если никто их не удаляет.
  2. Мониторьте операции анонимного списка. Журналы хранилища (Панель → Logs → Storage) записывают запросы LIST. Всплеск анонимных запросов списка против приватной корзины означает, что кто-то зондирует её извне.
  3. Установите политику хранения для эфемерных загрузок. Временные корзины (предварительный просмотр изображений, черновики загрузок) должны автоматически удаляться через 24-72 часа через запланированную функцию. Неопределённое хранение — это ответственность согласно обязательствам по минимизации данных GDPR / CCPA.
  4. Запускайте сканирование FixVibe ежемесячно. Проверка baas.supabase-storage-public зондирует корзины, которые отвечают на анонимные GET + LIST. Новые корзины добавляются; старые меняют видимость — только непрерывное сканирование ловит смещение.

Следующие шаги

Запустите сканирование FixVibe против вашего производственного URL — анонимные списки хранилища отображаются под baas.supabase-storage-public. Сочетайте этот чек-лист со сканером Supabase RLS для уровня таблиц и ключом сервисной роли Supabase, раскрытым в JavaScript для смежности раскрытия ключей. Для неправильных настроек хранилища у других поставщиков BaaS см. Сканер неправильных настроек BaaS.

// сканируйте вашу baas-поверхность

Найдите открытую таблицу раньше, чем это сделает кто-то другой.

Введите производственный URL. FixVibe перечислит поставщиков BaaS, с которыми взаимодействует ваше приложение, снимет отпечатки с их публичных конечных точек и сообщит, что неавторизованный клиент может прочитать или записать. Бесплатно, без установки, без карты.

  • Бесплатный тариф — 3 сканирования в месяц, без карты при регистрации.
  • Пассивное снятие отпечатков BaaS — не требуется проверка владения доменом.
  • Supabase, Firebase, Clerk, Auth0, Appwrite и другие.
  • Подсказки исправления ИИ для каждой находки — вставьте обратно в Cursor / Claude Code.
Чек-лист безопасности хранилища Supabase: 22 пункта — Docs · FixVibe