Ir al contenido

Buckets públicos en Supabase Storage: ¿están expuestos los archivos de tus usuarios?

Los avatares pueden ser públicos; los documentos, facturas y contratos, no. Cómo revisar los buckets y reglas de Supabase Storage y corregirlos sin romper nada.

4 min de lectura

La mayoría de las apps hechas con Lovable, Bolt y herramientas similares permiten al usuario subir algo: una foto de perfil, un recibo, un contrato, una foto de su documento para verificarlo. En Supabase, esos archivos van a Storage, organizados en buckets.

Cuando una herramienta de IA crea un bucket, a menudo lo hace público, porque así las imágenes se ven sin trabajo extra. Para un logo está bien. Para cualquier cosa personal, significa que cualquiera con el enlace puede descargar el archivo, y a veces cualquiera puede listar todos los archivos del bucket.

Esta guía es la mitad de Storage de nuestra revisión de Supabase en 10 minutos. Lleva más o menos lo mismo.

Buckets públicos y privados

Bucket público Bucket privado (el predeterminado)
Descargar un archivo Cualquiera con la URL, sin comprobar reglas Solo si una regla lo permite, o con un enlace firmado que caduca
Subir, borrar, mover Lo controlan tus reglas Lo controlan tus reglas
Listar los archivos Lo controlan tus reglas Lo controlan tus reglas

De esta tabla se deducen dos cosas:

  1. Un bucket público se salta tus reglas por completo en las descargas. Si las rutas de los archivos son predecibles (invoices/1043.pdf, ids/user-15.jpg), un desconocido puede adivinarlas.
  2. Hacer privado un bucket no sirve de nada si las reglas de storage.objects dejan que cualquiera lo lea todo. Las reglas de Storage son políticas normales de Row Level Security sobre esa tabla, y una regla demasiado amplia abre un bucket privado igual de bien.

Paso 1: lista tus buckets

En el panel de Supabase, abre el SQL Editor y ejecuta:

Buckets, visibilidad y límites de subida
select id, public, file_size_limit, allowed_mime_types
from storage.buckets
order by public desc, id;

Para cada fila con public = true, hazte una pregunta: ¿me parecería bien que todos los archivos de este bucket aparecieran en un buscador? Imágenes de productos, sí. Cualquier cosa que un usuario haya subido sobre sí mismo, no.

Mira también las dos últimas columnas. Si los límites están vacíos, cualquiera que pueda subir archivos puede guardar uno de cualquier tamaño y cualquier tipo en tu proyecto, a tu costa.

Paso 2: lee las reglas de Storage

Todas las reglas de storage.objects
select policyname, cmd, roles, qual, with_check
from pg_policies
where schemaname = 'storage' and tablename = 'objects'
order by cmd, policyname;

Qué buscar:

  • Una regla SELECT para anon o public cuya condición sea solo bucket_id = '…'. Permite a cualquiera listar y leer todos los archivos del bucket.
  • Una regla INSERT para anon, o para authenticated sin condición de carpeta. Permite a cualquier usuario subir archivos en cualquier sitio, incluso encima de las rutas de otros usuarios si también hay una regla UPDATE abierta.
  • Una regla DELETE que no vincula el archivo con su dueño.

El Security Advisor del panel también señala algunos de estos casos. Revísalo después de cada cambio.

Paso 3: compruébalo desde fuera

Con solo la URL del proyecto y la clave pública (en Project Settings → API Keys), intenta listar un bucket como lo haría un desconocido:

Listar archivos de un bucket con la clave pública
curl -X POST "https://YOUR-PROJECT.supabase.co/storage/v1/object/list/BUCKET_NAME" \
  -H "apikey: YOUR_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prefix": "", "limit": 20}'

Si devuelve nombres de archivos de un bucket que guarda archivos privados, el listado está abierto. Repite con cada bucket que contenga algo personal.

Cómo corregirlo

Mueve los archivos personales a un bucket privado

Crea un bucket privado (por ejemplo, documents) para todo lo que identifique a una persona o contenga datos del negocio. Deja un bucket público solo para los archivos que deben ser públicos.

Después, cambia cómo muestra la app esos archivos. En lugar de una URL pública permanente, pide a Supabase una URL firmada que caduca, por ejemplo en una hora. Con el cliente de JavaScript, es createSignedUrl(path, 3600). Los enlaces públicos antiguos dejan de funcionar al hacer el cambio, así que planifícalo junto con el cambio de código, no antes.

Dale a cada usuario su propia carpeta

El patrón habitual es subir cada archivo dentro de una carpeta con el ID del usuario, como documents/8f1c…/contract.pdf, y escribir reglas que lo comprueben:

Cada usuario lee y sube solo en su propia carpeta
create policy "Read own documents"
on storage.objects for select
to authenticated
using (
  bucket_id = 'documents'
  and (storage.foldername(name))[1] = (select auth.uid()::text)
);

create policy "Upload to own folder"
on storage.objects for insert
to authenticated
with check (
  bucket_id = 'documents'
  and (storage.foldername(name))[1] = (select auth.uid()::text)
);

Añade reglas de update y delete con la misma condición si los usuarios pueden reemplazar o eliminar sus archivos. Subir con la opción de sobrescribir activada requiere select, insert y update.

Buckets públicos: que se puedan leer, pero no listar

Un bucket público de avatares no necesita ninguna regla SELECT para las descargas, porque las URL públicas se saltan las reglas. Elimina las reglas SELECT amplias para anon en buckets públicos, salvo que la app de verdad necesite listar su contenido.

Pon límites

Dale a cada bucket un tamaño máximo de archivo y una lista de tipos permitidos (por ejemplo, image/png, image/jpeg, application/pdf). Lleva un minuto en la configuración del bucket y evita que tu Storage se convierta en hosting gratuito para otra persona.

Si tu app maneja documentos, identificaciones o registros de pago y quieres que alguien lo revise contigo, nuestro Security Check gratuito revisa Storage junto con las reglas de la base de datos, las claves y la autenticación, en modo de solo lectura y desde fuera.

SupabaseRow Level SecurityApps hechas con IA

Este artículo también está disponible en: EnglishPortuguês日本語中文