¿Cuándo se hace esto? (el momento exacto)
El momento es claro y no negociable: justo antes de publicar tu app o dársela a usuarios reales. Mientras pruebas tú solo en tu computadora, no pasa nada. Pero en el segundo en que tu app está en internet con datos de otras personas, sin estas cerraduras estás exponiendo todo. Y como la IA no lo hace sola (está pensando en que funcione, no en protegerla), te toca pedirlo explícitamente.
Cerradura 1 · RLS (que cada quien vea SOLO lo suyo)
Sin RLS, un usuario malicioso solo necesita cambiar un ID en la URL o en una petición para ver los datos de otra persona. Es la vulnerabilidad más común en apps con Supabase y PostgreSQL — y la más fácil de explotar. Por eso es la cerradura #1.
Estoy usando Supabase con PostgreSQL. Tengo estas tablas: [lista tus tablas y sus columnas de usuario, ej: posts (user_id), comments (post_id, user_id)]. Configura Row-Level Security para que cada usuario solo pueda ver/editar/borrar sus propios registros. Sigue las mejores prácticas oficiales de Supabase: - Activa RLS en TODAS las tablas (ENABLE ROW LEVEL SECURITY). - Crea políticas SEPARADAS para SELECT, INSERT, UPDATE y DELETE (NO uses FOR ALL). - IMPORTANTE para el rendimiento: envuelve la función de usuario como (SELECT auth.uid()), NO auth.uid() suelto, para que Postgres la evalúe una sola vez por consulta y no una vez por fila. - Restringe las políticas al rol authenticated (TO authenticated), no a public, para que los visitantes anónimos ni toquen la tabla. - NUNCA uses user_metadata en las políticas (el usuario lo puede modificar). - Agrega índices en las columnas que usan las políticas (ej. user_id). - Recuerda: para que un UPDATE funcione, también hace falta una política de SELECT. - Dame el SQL completo listo para pegar en el SQL Editor de Supabase, y explícame en una línea qué hace cada política.
auth.uid() directo en la política. PERO eso hace que Postgres lo evalúe una vez por cada fila — en una tabla de 100.000 filas, la diferencia es entre una consulta de 5 milisegundos y una de ¡5 segundos! El truco (documentado por Supabase): escríbelo como `(SELECT auth.uid())`. Así se evalúa una sola vez. Por eso el prompt de arriba lo pide explícito.- Crea dos usuarios de prueba en tu app.
- Inicia sesión con el usuario A y crea un registro.
- Inicia sesión con el usuario B: NO debe ver el registro de A.
- Revisa el Security Advisor de Supabase: te avisa si dejaste alguna tabla sin RLS.
Cerradura 2 · CORS (quién puede llamar a tu API)
Configura CORS en mi backend para que SOLO mi dominio de producción (https://mi-app.com) y localhost en desarrollo puedan hacer peticiones. - NO uses "*" como origen permitido en producción. - Permite solo los métodos que realmente uso (ej. GET, POST, PUT, DELETE). - Permite solo las cabeceras que necesito. - Explícame exactamente dónde poner cada cosa según mi stack: [dime tu framework/backend].
Access-Control-Allow-Origin: * con credenciales activadas. Es como dejar la puerta abierta y poner un cartel que dice "pasen, dejé las llaves dentro". Si tu IA te lo configuró así "para que funcionara rápido", arréglalo antes de publicar.Cerradura 3 · Security Headers (instrucciones de defensa al navegador)
Agrega los headers de seguridad recomendados a mi app: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security (HSTS) y Permissions-Policy. - Explícame qué hace cada uno en una línea simple. - Dame la configuración lista para mi framework: [dime cuál usas, ej. Next.js, Express]. - Empieza la Content-Security-Policy en modo report-only para no romper nada, y dime cómo endurecerla después. - Dime cómo verificar el resultado.
El hábito · revísalo antes de cada publicación
- RLS activado en todas las tablas con datos de usuarios (y probado con 2 cuentas).
- CORS restringido a tus dominios, nunca
*con credenciales. - Headers puestos y con nota A en securityheaders.com.
- Tus secretos (.env, llaves de API) NUNCA subidos a GitHub (ver la guía de GitHub de la serie).
- Revisaste el Security Advisor de Supabase y no quedan avisos en rojo.