← Guías por stack

Supabase

Seguridad para tu app con Supabase

Supabase expone tu base de datos a través de una API desde el minuto uno. Sin RLS bien puesto o con la clave equivocada en el frontend, cualquiera puede leer tus tablas. Comprueba lo básico antes de lanzar.

Riesgos típicos en Supabase

Tablas sin RLS

Row Level Security es la barrera principal. Una tabla nueva sin RLS queda abierta a lectura y escritura desde el navegador con la clave pública.

La service_role en el frontend

La clave service_role se salta toda la seguridad. Si acaba en el JavaScript del cliente, entrega el control total de tu base de datos.

Buckets de Storage abiertos

Un bucket público sirve cualquier archivo a quien tenga la URL. Los documentos privados deben ir en buckets protegidos con policies.

"Seguridad en Supabase" para una app hecha con IA son dos cosas distintas. La primera es la configuración del proyecto: lo que Supabase expone por diseño y lo que te toca cerrar a ti. La segunda es lo que Lovable, Bolt o Cursor hacen con Supabase cuando generan el código, que es donde aparecen casi todos los agujeros. Esta guía cubre las dos, con tres consultas SQL para auditar tu proyecto en un minuto, un prompt de revisión para tu IA y la forma de comprobar desde fuera que todo está cerrado.

Parte 1: cómo funciona la seguridad en Supabase

La API de datos es pública por diseño

Supabase pone delante de tu Postgres una API REST (PostgREST) que el navegador llama directamente con la clave anon (o la nueva clave publishable). Esa clave va en el JavaScript de todos tus usuarios y eso es normal: identifica el proyecto, no da permisos. Lo que decide quién puede leer o escribir cada fila es Row Level Security. La otra clave, service_role (o secret), se salta RLS por completo y solo puede vivir en un servidor. Si entiendes esta separación, entiendes el 80 % de la seguridad en Supabase.

RLS y políticas: la única barrera real

Una tabla nueva en el esquema public es accesible por la API desde que la creas. Sin RLS activado, cualquiera con la clave anon lee y escribe todas sus filas. Con RLS activado y sin políticas, nadie accede. Con políticas, accede quien ellas digan. Por eso la regla es: RLS en todas las tablas y una política explícita por operación (select, insert, update, delete). Los patrones concretos (cada usuario sus filas, pertenencia a un equipo, contenido público de solo lectura) están en qué revisar en Supabase antes de lanzar.

Lo que se salta el RLS sin que lo notes

  • Vistas. Por defecto una vista se ejecuta con los permisos de quien la creó, es decir, ignora el RLS de las tablas que consulta. Créalas con with (security_invoker = on).
  • Funciones security definer. Una función RPC marcada así corre como su dueño y lee lo que quiera. Úsalas solo cuando sea imprescindible, con set search_path = '' y revocando la ejecución a anon si no debe llamarla un visitante.
  • Esquemas expuestos. Todo lo que está en public (tablas, vistas y funciones) es alcanzable por la API. Si una función es interna, sácala a otro esquema.
  • La clave service_role. En el frontend, en un repositorio público o en un prompt pegado a una IA, es control total de la base de datos.

Auth: ajustes que conviene mirar

En Authentication tienes la confirmación de email (déjala activada), la lista de Redirect URLs permitidas (si está vacía o con comodines, un enlace mágico puede acabar en un dominio ajeno), los límites de peticiones, el captcha para registro y recuperación, la política de contraseñas, la protección contra contraseñas filtradas y el inicio de sesión anónimo (desactivado salvo que lo uses a propósito). Cada uno es una casilla; ninguno viene mal por defecto, pero la IA suele tocarlos para que "pasen las pruebas".

Storage, Realtime y Edge Functions

Los buckets públicos sirven cualquier archivo a quien tenga la URL: para subidas de usuarios usa buckets privados, políticas sobre storage.objects y URLs firmadas con caducidad, además de límite de tamaño y tipos permitidos por bucket. En Realtime, los cambios de Postgres respetan RLS, pero los canales de broadcast y presencia necesitan sus propias políticas si llevan datos privados. En Edge Functions, los secretos van con supabase secrets set, nunca en el código, y si desactivas la verificación de JWT para un webhook, valida la firma del remitente tú mismo.

El proyecto en sí

La cadena de conexión directa a Postgres (con la contraseña de la base de datos) no debe salir de tu servidor ni de tus variables de entorno. Activa el forzado de SSL, considera las restricciones de red si tienes plan de pago, y revisa de vez en cuando el Security Advisor del panel (Database, Advisors): detecta tablas sin RLS, vistas security definer, funciones con search_path mutable y tablas de auth expuestas.

Parte 2: lo que la IA hace mal con Supabase

Estos son los errores que más se repiten en apps generadas con Lovable, Bolt o Cursor, en orden de frecuencia:

  • Crear tablas sin RLS. El generador hace la migración, la app funciona y nadie activa nada. Es el origen de la CVE-2025-48757 de Lovable, que expuso datos de cientos de proyectos.
  • Políticas using (true). Cuando RLS bloquea algo, la IA "lo arregla" con una política que deja pasar todo. Es RLS desactivado con otro nombre.
  • service_role en el navegador. Una consulta falla por permisos y la solución rápida es cambiar la clave. Busca service_role en tu bundle ahora mismo.
  • Funciones security definer para saltarse errores. Mismo patrón: en vez de escribir la política correcta, se envuelve la consulta en una función que corre como superusuario.
  • Buckets públicos para archivos de usuarios. Avatares vale; facturas, documentos o exportaciones, no.
  • Campos de privilegio editables por el usuario. Un role o is_admin en la tabla profiles con una política de update que no excluye esas columnas. Cualquiera se hace administrador desde la consola del navegador.
  • Desactivar la confirmación de email o los límites para que las pruebas vayan más rápido, y olvidarlo en producción.

Tres consultas SQL para auditar tu proyecto

Pégalas en el editor SQL del panel. La primera lista las tablas públicas sin RLS; lo ideal es que no devuelva nada:

select n.nspname as schema, c.relname as table
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r' and n.nspname = 'public' and not c.relrowsecurity;

La segunda encuentra políticas que dejan pasar todo:

select tablename, policyname, cmd, roles
from pg_policies
where schemaname = 'public' and (qual = 'true' or with_check = 'true');

Una política así en una tabla de contenido público de solo lectura puede ser correcta; en profiles, orders o messages, no. La tercera lista las funciones que corren con permisos de su dueño:

select p.proname, pg_get_function_identity_arguments(p.oid) as args
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.prosecdef;

Cada resultado debería tener un motivo que puedas explicar. Si no lo tiene, quítale el security definer o muévela fuera de public.

Un prompt de revisión antes de desplegar

Pégalo en Lovable, Bolt o Cursor con acceso a tu esquema y a tu código:

Revisa la seguridad de esta app con Supabase y no cambies nada todavía. Responde con una lista:
1. Tablas del esquema public sin RLS activado.
2. Políticas con using (true) o with check (true), y si están justificadas.
3. Columnas de privilegio (role, is_admin, plan, credits) que un usuario pueda modificar con las políticas de update actuales.
4. Vistas sin security_invoker y funciones security definer, con el motivo de cada una.
5. Cualquier uso de la clave service_role o de la cadena de conexión de Postgres fuera del servidor.
6. Buckets públicos que contengan archivos de usuarios.
7. Ajustes de Auth que se hayan desactivado (confirmación de email, límites, captcha) y redirect URLs con comodines.
Para cada punto, propón el SQL o el cambio exacto, y espera mi confirmación.

Comprueba el resultado desde fuera

La prueba definitiva no es el panel, es lo que responde la API a un desconocido. Con tu URL de proyecto y la clave anon (las dos están en el código del navegador, así que un atacante también las tiene):

curl -s "https://TU_PROYECTO.supabase.co/rest/v1/profiles?select=*" -H "apikey: TU_CLAVE_ANON" -H "Authorization: Bearer TU_CLAVE_ANON"

Si devuelve filas, esa tabla está abierta. Si devuelve [] o un error de permisos, el RLS funciona. Repite con cada tabla sensible y con un usuario normal autenticado para confirmar que solo ve lo suyo. Scanaris hace esta comprobación por ti: detecta la URL y la clave de Supabase en tu bundle, prueba las tablas habituales sin sesión, busca la clave service_role y la cadena de conexión en el JavaScript, revisa dónde guarda tu app el token de sesión y te da un fix prompt por cada hallazgo. Si tu app la generó Lovable, mira también la guía de seguridad para Lovable; si el problema son las claves en el cliente, secretos que tu app filtra en el navegador.

Preguntas frecuentes

¿Es un problema que la clave anon esté en el navegador?

No. Está diseñada para eso y solo identifica el proyecto. Lo que protege los datos es RLS. La que nunca puede estar en el navegador es service_role (o la nueva clave secret).

¿Las claves nuevas publishable y secret cambian algo?

Cambian el formato y permiten rotarlas sin tocar el JWT, pero el modelo es el mismo: la publishable es pública y pasa por RLS; la secret se lo salta y vive en el servidor. Las claves antiguas siguen funcionando mientras no las deshabilites.

Si mi app la hizo Lovable o Bolt, ¿tengo que entrar igualmente al panel de Supabase?

Sí. La herramienta genera el esquema, pero RLS, políticas, buckets y ajustes de Auth son tuyos. Diez minutos con las tres consultas de arriba y el Security Advisor te dicen en qué estado está el proyecto.

Checks relacionados

¿Tu app Supabase pasa estos checks?

Escanéala gratis y recibe un prompt de arreglo por cada fallo que encontremos.

Escanear mi web gratis