Saltar al contenido

Fundamentos de seguridad

Tu bucket de Supabase Storage es público. ¿Es un problema?

Un bucket público de Supabase Storage significa que quien tenga la URL de un archivo puede abrirlo. No significa que alguien pueda listar lo que hay dentro.

Vlad Tkachenko8 min de lectura
Documentos dentro de un contenedor. Una barra cubre casi toda la abertura y se corta antes de terminar, dejando cuatro de ellos en el hueco.

En resumen

  • Un bucket público de Supabase Storage significa una sola cosa: quien tenga la URL de un archivo puede abrirlo sin iniciar sesión. No dice nada sobre si puede ver qué más hay dentro.
  • Listar lo concede una política de acceso y no el interruptor de público, así que un bucket privado puede ser listable y uno público puede no serlo.
  • Listar es lo que hay que arreglar hoy, porque le ahorra a un desconocido el trabajo de adivinar un solo nombre de archivo.

Alguien abrió tu aplicación, estuvo un minuto hurgando en ella y te mandó un mensaje: tu bucket de Supabase Storage es público y cualquiera puede listar lo que hay dentro.

Son dos afirmaciones distintas. Una de ellas es, probablemente, un ajuste que elegiste a propósito y que deberías conservar. La otra merece una tarde.

Aquí está la parte que guía tras guía cuenta mal: público y listable son dos interruptores distintos, y el alarmante no es el que todo el mundo te dice que cambies. Poner un bucket en privado cierra el primero. Puede dejar el segundo abierto de par en par, y un bucket que no ha sido público en su vida puede ser listable esta misma tarde.

¿Es un problema de seguridad un bucket público de Supabase Storage?

Por sí solo, no. Significa una cosa muy concreta, y esa cosa es muy a menudo la que querías.

Marcar un bucket como público le da a cada archivo que hay dentro una URL que funciona sin iniciar sesión. Esa es toda la función. Así carga el avatar de un comentario para quien no tiene cuenta, así aparece tu logotipo en un correo, así ve la foto de un producto quien todavía está decidiendo si se registra.

Un bucket público se parece más a un número de teléfono que no está en la guía que a una puerta sin cerrar. La línea conecta con cualquiera que marque, y no hay ningún listín donde buscarlo. Que eso esté bien depende de una sola pregunta: con qué facilidad llegaría alguien a ese número sin que nadie se lo diera.

Supabase crea los buckets nuevos en privado y pone un aviso junto al interruptor. Un bucket público es, por tanto, algo que alguien encendió: tú, o tu builder, para que funcionase una función de subida.

Qué enciende realmente el interruptor de público

Una fila de la tabla de abajo, y nada más de ella.

Lo que alguien intentaBucket públicoBucket privado
Abrir un archivo cuya URL exacta ya tieneFunciona, sin iniciar sesiónNecesita una política o un enlace firmado
Pedir la lista del contenido del bucketSolo si una política lo permiteSolo si una política lo permite
Subir un archivoSolo si una política lo permiteSolo si una política lo permite
Borrar o reemplazar un archivoSolo si una política lo permiteSolo si una política lo permite

La propia página de ayuda de Supabase lo dice con toda la claridad posible: un bucket público significa que existe una URL pública con la que descargar el archivo, y cualquier otra operación sigue teniendo que cumplir las políticas de ese bucket.

Así que el interruptor es un control más pequeño de lo que sugiere su nombre. Tres de esas cuatro filas se deciden en otro sitio completamente distinto, en Storage → Policies, y el mensaje que recibiste iba casi con seguridad sobre la segunda.

El interruptor de arriba cambia una cosa. La caja de debajo decide si un desconocido recibe la lista, y lo hace igual tanto si el bucket es público como si es privado.

Por qué un bucket privado puede seguir siendo listable

Porque listar lo concede una política de acceso, y la política que casi todo el mundo pega se lo concede a todos.

Toda lectura de Storage pasa por Row Level Security en una tabla llamada storage.objects. Listar un bucket es una lectura, archivada bajo SELECT, igual que descargar un archivo. La propia guía rápida de Supabase enseña una política con esta forma, que es de donde suelen salir las copias:

create policy "Public Access"
  on storage.objects for select
  using ( bucket_id = 'public' );

Lee lo que dice. Cualquier petición puede leer cualquier cosa de ese bucket. No pregunta quién pregunta, y no pregunta si el archivo tiene algo que ver con quien lo pide. Leer incluye descargar un archivo, e incluye entregar la lista de archivos, porque son el mismo permiso con dos sombreros.

El interruptor del propio bucket no entra nunca en la ecuación. Una política así sobre un bucket privado lo vuelve listable, y el panel seguirá describiéndolo, con toda razón, como privado.

La palabra que hace el daño es «cualquiera». Una petición que lleve la clave publicable que viaja dentro del código de tu aplicación cumple esa política sin esfuerzo, y esa clave está pensada para que la lea todo el mundo, que es justo por lo que se supone que la política es la parte que trabaja. Es la misma forma que el problema de al lado, donde Row Level Security está encendida y aun así lo permite todo.

Qué saca alguien de una lista de tus archivos

Nombres, sobre todo. Que es bastante más de lo que suena.

Los nombres de archivo suelen describir su contenido, porque alguien los eligió pensando en lo que había dentro: factura-marzo-acme.pdf, pasaporte-frente.jpg, nomina-final-v2.xlsx. Una lista de esos es un resumen razonable de tu negocio, y su longitud dice más o menos cuántos clientes tienes. Si además el bucket es público, cada nombre de esa lista es un enlace que funciona.

Volvamos al número de teléfono. No estar en la guía vale algo justo hasta que se publica el listín, y después nunca importó lo difícil que fuera adivinar el número.

Sin listado, un desconocido tiene que llegar a un nombre de archivo de alguna manera. Con él, le entregan todos los nombres que tienes.

La forma en que esto aflora es de lo más corriente. Un documento de un cliente aparece delante de otro. Alguien te repite el nombre de un archivo que no tenía manera de conocer. Una carpeta de subidas se copia entera, por un rastreador automático que iba leyendo todos los proyectos de Supabase que encontraba.

Nuestro escaneo gratuito le pide un listado a tu Storage usando nada más que la clave que ya está en el código de tu aplicación, y te dice qué buckets respondieron con contenido. Lee nombres y nunca descarga un archivo. Tarda unos 20 segundos y no necesita cuenta: escanea tu aplicación.

Cómo saber cuál de los dos casos tienes

Dos comprobaciones en el panel de Supabase, y la segunda es de lo que iba de verdad el mensaje.

Storage → Buckets. Los públicos están marcados como públicos. Pregúntate por cada uno si absolutamente todos los archivos que contiene son algo que le enseñarías a un desconocido. No la mayoría de los archivos. Todos, incluido lo que aterrice ahí la semana que viene desde una función que aún no has creado.

Storage → Policies. Lee cada política SELECT que toque el bucket. Una condición que solo menciona el nombre del bucket deja que cualquiera lo lea. Una condición que compara al dueño del archivo con quien hace la petición está haciendo trabajo de verdad. Una lista de políticas vacía sobre un bucket privado significa que no se lee nada en absoluto, y eso es restrictivo y seguro.

Si tu aplicación tiene que enseñarle un archivo privado a la persona correcta, la herramienta para eso es un enlace firmado: tu servidor le pide a Supabase una URL que funciona durante un número fijo de minutos y luego deja de funcionar. Así el archivo sigue siendo privado y aun así llega a la página.

Cuándo público es la respuesta correcta

Más a menudo de lo que un artículo de seguridad suele admitir, y conviene decirlo.

Si el archivo es para todo el mundo, público es lo correcto, y dar un rodeo te compra una aplicación más lenta y más código que mantener. Avatares, logotipos, imágenes de portada, cualquier cosa que deba ver alguien sin sesión iniciada: a un bucket público, y a dejar de pensar en ello.

Lo único que hay que hacer incluso ahí es dejar el listado apagado. Un bucket público lleno de avatares es una función. El mismo bucket entregando la lista completa de todos los que han subido uno alguna vez es otra cosa, y no la pediste.

Si el archivo pertenece a una persona concreta, va a un bucket privado con una política que comprueba quién pregunta, y llega a la página por un enlace firmado. La prueba es una sola pregunta: ¿para quién es este archivo? Para todos, o para una cuenta con nombre y apellidos.

Qué hacer esta semana

Qué hacer

  • Abre Storage → Buckets y apunta cuáles son públicos. Decide en cada uno si guarda archivos para todo el mundo o archivos para una sola persona.
  • Lee cada política SELECT sobre storage.objects. Una condición que solo nombra el bucket entrega la lista de archivos a cualquiera; esa es la que hizo verdadero el mensaje que recibiste.
  • Mueve todo lo que pertenezca a un único usuario a un bucket privado y sírvelo por enlaces firmados, que caducan solos.
  • Deja de nombrar los archivos subidos según su contenido. Un identificador aleatorio no cuesta nada y hace que una lista filtrada entregue mucho menos de lo que habría entregado.
  • Haz copia de tu Storage aparte de la de tu base de datos. Tus archivos subidos no están ni en tu código ni en tus tablas de Postgres, así que nada de lo que copie esos dos está copiando tus archivos.

Abre Storage → Policies en tu proyecto y lee lo que hay ahí antes de tocar un solo interruptor. Esa única pantalla es la que decide si la segunda mitad del mensaje que recibiste era cierta. La lista de seguridad de 10 minutos lo cubre junto al resto de lo que una aplicación recién lanzada suele dejar abierto, y si un bucket resultó ser legible, qué alcanza un desconocido en tu base de datos es lo siguiente que leer, ya que la política que entrega una lista de archivos tiene la misma forma que la que entrega una tabla.

Preguntas frecuentes

Me dicen que mi bucket de Supabase Storage es público. ¿Debo hacerlo privado?

No hasta que sepas qué hay dentro. Los buckets públicos existen por una razón: fotos de perfil, logotipos, imágenes de producto y cualquier otra cosa que tu aplicación muestre a alguien que no ha iniciado sesión. Para esos archivos, público es el ajuste correcto. Para facturas, documentos de identidad, exportaciones o cualquier cosa que pertenezca a una persona concreta, es el equivocado, y lo que quieres entonces es privado más enlaces firmados. La pregunta nunca fue si público es malo. Es si esos archivos en concreto estaban pensados para que los viera cualquiera que preguntase.

¿Cuál es la diferencia entre un bucket público y uno listable?

Público decide si un archivo se abre para alguien que ya tiene su URL. Listable decide si un desconocido puede pedirle a tu proyecto el contenido entero de un bucket y recibir una respuesta. Se configuran en sitios distintos del panel de Supabase, y el segundo es el que convierte una conjetura en un directorio.

¿Puede alguien adivinar las URL de los archivos de mi bucket público?

Depende por completo de cómo los nombre tu aplicación. Si los sube con el nombre original, o con algo ordenado como factura-4.pdf, entonces sí, y cuesta muy poco esfuerzo. Si los sube con un identificador largo y aleatorio, adivinar deja de ser práctico. Por eso mismo pesa tanto listar: elimina el paso de adivinar por completo.

¿Cómo impido que la gente liste mi bucket?

Listar pasa por una política de Row Level Security sobre la tabla storage.objects, así que ahí es donde se arregla y no en el bucket. Abre Storage → Policies en tu panel de Supabase y lee cada política SELECT que toque el bucket. Una cuya única condición sea el nombre del bucket deja que cualquier petición lo lea, incluida su lista de archivos. Estréchala para que compare el archivo con quien lo pide, o quítala y reparte enlaces firmados desde tu servidor.

¿Mi copia de seguridad de la base de datos incluye los archivos subidos?

No. Storage vive fuera de tu base de datos Postgres, así que una copia de la base contiene las filas que apuntan a tus archivos y ninguno de los archivos. Restaurarla te devuelve una tabla llena de enlaces a cosas que ya no están. Tus archivos subidos hay que copiarlos aparte, y la mayoría de la gente se entera el día en que importa.

Escrito por

Vlad Tkachenko

Fundador de Reeve

Me paso el día mirando apps creadas con Lovable, Bolt, v0, Cursor y Replit, y la corta lista de errores que aparecen en ellas una y otra vez.

Más sobre el autor

Sigue leyendo

¿No sabes cómo está tu propia app?

Haz un escaneo gratuito y obtén una nota clara de la A a la F en unos 20 segundos. Sin cuenta y sin tarjeta.

Analizar mi app gratis

Comprobación externa automatizada, no una auditoría completa. La ausencia de hallazgos no es garantía de seguridad.