Fundamentos de seguridad
Qué claves de API son seguras en el navegador y cuáles no
Tu clave anon de Supabase está pensada para ser pública. Tu clave service_role no, y se salta todas las reglas que pongas. Cómo distinguirlas.
En resumen
- Encontrar una clave de API en el código de tu app no es automáticamente un problema. Algunas claves están pensadas para estar ahí.
- Las claves publicables son seguras en el navegador. Las secretas no: una clave secreta en el navegador es una caja abierta.
- Dos comprobaciones las separan en menos de un minuto: leer el prefijo y, en una clave antigua de Supabase, descodificar el rol.
Si has creado tu app con Lovable, Bolt, v0, Cursor o Replit, tarde o temprano alguien abrirá tu web, pulsará F12 y te dirá que tu clave de API está «expuesta». Es una noticia inquietante sobre una app cuyo código no puedes leer.
Aquí está lo que guía tras guía cuenta mal: algunas de esas claves tienen que estar ahí. Tratar cada clave visible como una brecha lleva o bien a asustarse sin motivo o, mucho peor, a acostumbrarse a ignorar el aviso, y a ignorarlo también el día en que sí importa.
¿Es malo que mi clave de API sea visible?
Normalmente no. Depende por completo de qué clave sea.
Todo servicio que habla con un navegador reparte dos tipos distintos de credencial. Una es una dirección. La otra es el juego de llaves del edificio. Las dos se llaman «clave de API», y ahí está casi toda la confusión.
Una dirección se puede publicar sin riesgo. Solo indica a qué proyecto pertenece una petición; la comprobación de permisos ocurre en otro sitio. Un juego de llaves no, porque es la comprobación de permisos: quien lo tenga puede hacer todo lo que permita, desde donde sea.
Tu app necesita la dirección en el navegador para funcionar. Nunca debería necesitar ahí el juego de llaves.
Por qué tu app envía claves al navegador
Porque la petición sale del navegador de tu visitante, no de tu servidor.
Cuando tu app carga la lista de pedidos de tus usuarios, esa petición sale directamente del navegador de la visitante hacia tu proveedor de base de datos. Tiene que decir a qué proyecto pertenece, y ese identificador tiene que estar en la página, porque es desde ahí desde donde se hace la petición.
No existe ninguna versión en la que ese identificador sea secreto. Llega a cada visitante por diseño. Y justo por eso los proveedores parten sus credenciales en dos: saben que una de ellas va a ser pública, así que la hicieron inofensiva.
La seguridad no viene de esconder la dirección. Viene de las reglas que pones al otro lado: en el caso de Supabase, Row Level Security, que decide fila a fila quién puede ver qué. Esas reglas son lo que merece la pena revisar, y activarlas no es lo mismo que estar protegido.
Las dos familias de claves
| Proveedor | Clave | ¿Segura en el navegador? | Qué hace |
|---|---|---|---|
| Supabase | sb_publishable_…, o anon en proyectos antiguos | Su sitio | Dice de qué proyecto se trata. Cada petición sigue filtrándose con tus reglas de Row Level Security. |
| Supabase | sb_secret_…, o service_role en proyectos antiguos | Nunca | Se salta Row Level Security por completo. Lee y escribe cada fila de cada tabla, digan lo que digan tus reglas. |
| Stripe | pk_live_… (publicable) | Su sitio | Crea formularios de pago. No puede mover dinero ni leer clientes. |
| Stripe | sk_live_… (secreta) | Nunca | Acceso total a la cuenta: cobros, reembolsos, transferencias, fichas de clientes. |
| OpenAI / Anthropic | cualquier clave | Nunca | No existe una variante publicable. Cada clave factura directamente a tu cuenta. |
El patrón vale más allá de estos tres. Si un proveedor ofrece un solo tipo de clave, da por hecho que es secreta y que su sitio es un servidor.
Cómo distinguirlas en 60 segundos
En Stripe, y en la mayoría de proveedores, basta con el prefijo. pk_ es
publicable y segura. sk_ es secreta y no lo es. Algunos proveedores meten
_test_ o _live_ en medio: una clave de prueba filtrada es un problema mucho
menor que una de producción, pero renueva las dos.
En Supabase depende de lo antiguo que sea tu proyecto. Supabase ha emitido dos formas distintas de clave, y las dos siguen hoy en apps que funcionan.
Proyectos nuevos: lee el prefijo, igual que en Stripe. sb_publishable_… es
la que tiene su sitio en tu app. sb_secret_… es la que hay que renovar hoy. No
hay nada que descodificar: para qué sirve la clave está escrito delante.
Qué ha cambiado y qué significa para tu app, por
si aún no te has cruzado con ellas.
Proyectos antiguos: hay que mirar dentro de la clave. El par original, anon
y service_role, son JWT: tres bloques de galimatías separados por puntos, y el
del medio es información legible, no cifrado. Ahí pone el rol de la clave en
texto claro.
En ninguno de los dos casos hace falta una herramienta. Abre Settings → API Keys en el panel de Supabase y te dice cuál es cuál. Si prefieres comprobar la clave que has encontrado de verdad en tu app, la sección del medio de una clave antigua se descodifica en algo así.
{
"iss": "supabase",
"ref": "abcdefghij…",
"role": "anon", ← esta es la palabra que importa
"iat": 1750000000
}
"role": "anon" es la segura. "role": "service_role" es la que hay que
renovar hoy. En una clave antigua esa única palabra es toda la diferencia, y es
la razón de que un escáner que solo busca cadenas con forma de clave te dé ruido
en lugar de una respuesta: no puede decirte cuál de dos claves idénticas has
publicado.
Si prefieres no repasar tu app clave por clave, nuestro escaneo gratuito lee tu web en vivo y te dice cuáles de estas se ven desde fuera. Tarda unos 20 segundos y no necesita cuenta: escanea tu app.
Qué pasa de verdad cuando se filtra una clave secreta
Cada fila de tu base de datos pasa a ser legible y modificable por quien haya
encontrado la clave, incluidas tablas que nunca expusiste a tu app y los datos
personales de tus usuarios. Row Level Security no se aplica a una clave secreta
de Supabase (sb_secret_…, o service_role en un proyecto antiguo). Ese es el
propósito de la clave.
El daño tampoco es teórico. Se suele descubrir como un mensaje de soporte sobre datos que cambiaron solos, o como una tabla que de pronto está vacía.
Una clave sk_live_ de Stripe filtrada significa reembolsos, cobros y fichas de
clientes. Una clave filtrada de un proveedor de IA significa una factura, a veces
muy grande, que llega antes de que nadie se dé cuenta.
Nada de esto exige un atacante sofisticado. Hay escáneres automáticos rastreando webs públicas en busca exactamente de estas cadenas, y no necesitan saber quién eres para encontrar la tuya.
Si quieres la versión concreta de qué revisar en tu plataforma, tenemos una guía en lenguaje claro para apps con Supabase.
Qué hacer ahora mismo
Qué hacer
- Localiza cada clave de tu app e identifícala. El prefijo en Stripe y en las claves nuevas de Supabase; el campo
roledentro de las antiguas de Supabase. - Si encuentras una clave secreta en el navegador, renuévala primero. Borrarla del código no cierra la puerta: el valor antiguo sigue en tu historial de versiones y en las copias en caché de tu web.
- Mueve a un servidor lo que necesitaba esa clave: una edge function, una ruta serverless, cualquier cosa que no sea el navegador.
- Activa Row Level Security en todas las tablas y luego comprueba que funciona de verdad. Una clave publicable (
sb_publishable_…oanon) solo es segura gracias a esas reglas; sin ellas lee toda tu base de datos. - Revisa facturación y registros tras cualquier exposición de una clave secreta. Renovar detiene lo que venga después, no lo que ya pasó.
Si prefieres ir punto por punto, la lista de seguridad en 10 minutos cubre esto y las demás cosas que conviene desactivar en una app recién lanzada.
Y si una filtración llega a vaciar una tabla, que la recuperes depende por completo de qué estabas respaldando.
Preguntas frecuentes
Me dicen que mi clave de API está expuesta. ¿Debo preocuparme?
No hasta saber de qué clave se trata. Si empieza por pk_ o es una clave anon de Supabase, está pensada para ser pública y no pasa nada. Si empieza por sk_ o es una clave service_role, renuévala ya y luego mira a qué tenía acceso.
¿Puedo esconder la clave para que nadie la encuentre?
No. Todo lo que tu navegador puede usar, un visitante puede leerlo: minificar, renombrar u ofuscar solo frena a alguien unos segundos. La solución nunca es esconder una clave secreta en el navegador, sino moverla a un servidor o usar una clave publicable que ya era segura de exponer desde el principio.
¿Por qué Supabase me da una clave que cualquiera puede leer?
Porque no es la clave anon la que protege tus datos. Solo indica con qué proyecto estás hablando. La protección real es Row Level Security, que decide fila a fila qué puede ver cada visitante. Por eso una clave anon con RLS activado no es un problema, y esa misma clave sin RLS es una base de datos abierta.
Mi clave de Supabase empieza por sb_publishable_. ¿Es lo mismo que la clave anon?
Hace el mismo trabajo. Supabase cambió el nombre de sus claves: sb_publishable_ sustituye a la clave anon y sb_secret_ sustituye a service_role. Así que una clave que empieza por sb_publishable_ está pensada para vivir en tu app, y una que empieza por sb_secret_ nunca lo está. Los proyectos antiguos siguen llevando las claves anon y service_role originales, y esas siguen funcionando; puede que incluso veas los dos pares juntos en tu panel.
Ya pegué una clave secreta en mi app. ¿Y ahora qué?
Renuévala primero: en el panel del proveedor, genera una clave nueva y revoca la antigua. Eso es lo que cierra de verdad la puerta; quitarla del código no basta, porque el valor antiguo sigue en tu historial de versiones y en las copias en caché. Después mueve a un servidor lo que necesitaba esa clave y revisa la facturación y los registros por si hay algo que no hiciste tú.