Saltar al contenido

Cómo asegurar tu app de Lovable, Bolt o v0 antes de lanzar

Checklist práctico de seguridad para apps generadas con Lovable, Bolt o v0. Variables expuestas, RLS, API keys, headers. Antes de hacer público, hacé esto.

Tutoriales Equipo VibeAuditt 7 min de lectura

Lovable, Bolt y v0 generan apps que funcionan en minutos. Pero “funciona” y “es seguro lanzarla” son dos cosas distintas. Este es el checklist que aplicamos antes de cada launch.

TL;DR

  1. Por qué importa este checklist: Escape.tech (2025) analizó 5.600 apps vibe-coded y halló más de 2.000 vulnerabilidades, 400+ secretos expuestos y 175 casos de datos personales. Casi todo eso se previene antes de lanzar.
  2. Buscá claves en el bundle y movelas a backend
  3. Habilitá Row Level Security en Supabase para cada tabla
  4. Configurá headers de seguridad (CSP, HSTS, X-Frame-Options)
  5. Revisá CORS, source maps, console.log en producción
  6. Validá inputs en servidor, nunca solo en cliente
  7. Audita el resultado con una herramienta automatizada

Paso 1 — Audita el bundle JavaScript

El error más destructivo y más común: claves de servicios pagos terminan en el código que descarga el browser.

Después de hacer build (npm run build o equivalente), abrí los archivos .js de la carpeta dist/ o .output/ y buscá:

# Patterns más comunes
grep -r "sk_live_" dist/      # Stripe live key
grep -r "sk-proj-" dist/      # OpenAI project key
grep -r "service_role" dist/  # Supabase service role
grep -r "eyJ" dist/           # JWTs (los reales empiezan así)
grep -r "AKIA" dist/          # AWS access key
grep -r "ghp_" dist/          # GitHub personal access token
grep -r "xoxb-" dist/         # Slack bot token

Si aparecen, rotá la clave inmediatamente en el dashboard del servicio y mové la lógica que la usa a un endpoint backend.

Convención de nombres por builder

Cada framework tiene su prefijo para variables que SÍ se exponen al cliente:

  • Vite / Lovable: solo las que empiezan con VITE_* van al bundle
  • Next.js (v0): solo NEXT_PUBLIC_*
  • Bolt / SvelteKit: solo PUBLIC_*
  • Astro: solo PUBLIC_*

Cualquier variable sin esos prefijos queda server-side. Si necesitás que el frontend acceda a una clave (Stripe publishable key, Supabase anon key, Google Maps), usá el prefijo correcto. Si la clave es secreta, nunca le pongas el prefijo público.

Vulnerable (.env):

VITE_SUPABASE_SERVICE_ROLE_KEY=eyJ...   # ❌ "VITE_" expone la service role
VITE_STRIPE_SECRET=sk_live_...          # ❌ se va al bundle
VITE_OPENAI_KEY=sk-proj-...             # ❌ idem

Seguro:

# Cliente (van al bundle)
VITE_SUPABASE_URL=https://xxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_...

# Servidor (NO van al bundle, usar en endpoints backend)
SUPABASE_SERVICE_ROLE_KEY=eyJ...
STRIPE_SECRET_KEY=sk_live_...
OPENAI_API_KEY=sk-proj-...

Paso 2 — Row Level Security en Supabase

Si tu app usa Supabase (caso de >80% de los proyectos Lovable/Bolt), este paso es el más importante. La anon key está en el bundle por diseño; lo que protege tus datos son las políticas RLS.

Activá RLS en todas las tablas

Por cada tabla en tu schema, en el SQL editor de Supabase:

ALTER TABLE nombre_tabla ENABLE ROW LEVEL SECURITY;

Si no creás ninguna política después, la tabla queda invisible para clientes anónimos y autenticados. Eso es seguro por default (deny-all).

Agregá políticas explícitas

Para que tu app funcione, necesitás permitir lo justo:

-- Ejemplo: tabla "profiles"
CREATE POLICY "Los usuarios ven solo su perfil"
  ON profiles FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "Los usuarios actualizan solo su perfil"
  ON profiles FOR UPDATE
  USING (auth.uid() = user_id);

CREATE POLICY "Los usuarios crean solo su propio perfil"
  ON profiles FOR INSERT
  WITH CHECK (auth.uid() = user_id);

Evitá políticas peligrosas

Estas políticas son equivalentes a no tener RLS:

-- ❌ Permite todo a cualquiera
CREATE POLICY "open" ON tabla FOR ALL USING (true);

-- ❌ Permite a cualquier autenticado, sin distinción
CREATE POLICY "auth_open" ON tabla FOR ALL
  USING (auth.role() = 'authenticated');

La segunda es trampa: cualquiera puede crearse cuenta en tu Supabase con signUp, lo que vuelve authenticated la condición trivial.

Más detalle en nuestro post de Supabase RLS.

Paso 3 — Headers de seguridad HTTP

Por default, ni Lovable ni Bolt ni v0 configuran headers de seguridad. Tu hosting (Cloudflare Pages, Vercel, Netlify) los puede agregar con un archivo de configuración.

Cloudflare Pages

Creá public/_headers:

/*
  Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co https://api.stripe.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()

Vercel

En vercel.json:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Content-Security-Policy", "value": "default-src 'self'; ..." },
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
      ]
    }
  ]
}

Netlify

En netlify.toml:

[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; ..."
    Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"

CSP es el más delicado. Empezá con una política amplia ('unsafe-inline' en script-src) y andá ajustando viendo qué bloquea en consola. Reportá errores con report-uri o report-to y leelos en producción.

Más detalle en security headers HTTP.

Paso 4 — Configuración del build

Stripeá console.log y debugger en producción

Si usás Vite (Lovable, Bolt):

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  esbuild: {
    drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
  },
});

Para Next.js (v0):

// next.config.js
module.exports = {
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },
};

Desactivá source maps públicos

Source maps en producción dejan tu código original accesible. O los sacás, o los enviás a un servicio privado (Sentry):

// vite.config.ts
export default defineConfig({
  build: { sourcemap: false }, // o 'hidden' para uploadear a Sentry y no publicar
});
// next.config.js
module.exports = { productionBrowserSourceMaps: false };

Verificá que no se sirve .env ni .git

Después del build, antes de deployar, probá en local:

npx serve dist
# o
npx vercel dev

Navegá a http://localhost:3000/.env, /.env.local, /.git/HEAD. Deberían dar 404. Si dan otra cosa, hay archivos que no querés exponer en tu dist/.

Paso 5 — Validación de inputs

Lovable, Bolt y v0 generan código que valida en cliente (required, pattern, etc.) pero rara vez valida en servidor. Cualquier usuario con curl evita la validación cliente.

Vulnerable (solo cliente):

// Solo cliente — válido pero insuficiente
<input type="email" required pattern="..." />

Seguro (cliente + servidor):

// Cliente: UX
<input type="email" required pattern="..." />

// Servidor (Hono, Next API, etc.) con Zod
import { z } from 'zod';
const Schema = z.object({
  email: z.string().email().max(255),
  message: z.string().min(1).max(5000),
});

app.post('/api/contact', async (c) => {
  const data = Schema.parse(await c.req.json()); // throws 400 si inválido
  // ...
});

CSRF en endpoints de mutación

Si usás cookies para autenticación, cada POST/PUT/DELETE que cambia estado necesita protección CSRF. Las opciones más simples:

  • SameSite=Lax o SameSite=Strict en la cookie de sesión (defensa principal)
  • Header personalizado (e.g. X-Requested-With) que el frontend manda y el server verifica
  • Token CSRF rotado por sesión

Si tu API solo acepta JWTs en el header Authorization, no necesitás CSRF (los headers customizados no se envían cross-origin).

Paso 6 — Pre-launch checklist final

Antes de hacer público, corré este checklist:

  • Bundle JS no contiene sk_live_, sk-proj-, service_role, eyJ, AKIA, ghp_, ni similares
  • Todas las tablas Supabase tienen RLS habilitado con políticas explícitas
  • Probaste consultas Supabase sin estar logueado desde la consola y devuelve solo datos públicos (o nada)
  • Headers configurados: CSP, HSTS preload, X-Frame-Options, X-Content-Type-Options, Referrer-Policy
  • CORS no es * con credentials: true
  • Source maps no se sirven en producción
  • console.log y debugger removidos en build de producción
  • /.env, /.git/HEAD retornan 404
  • Inputs validados en servidor con Zod o similar
  • Endpoints API verifican autenticación antes de devolver datos
  • Auditaste el resultado con VibeAuditt para tener un segundo par de ojos automatizado

Si el último punto te falla, mejor descubrirlo ahora que cuando alguien reporta un breach en Twitter. La auditoría gratis incluye los 12 checks que cubren los errores arriba; el reporte detallado en PDF (plan Pro) suma severidad por finding y recomendación concreta.

Para deep-dive específico por stack, revisá Lovable, Bolt, v0, Cursor y Supabase.

// preguntas frecuentes

Preguntas frecuentes

¿Es seguro publicar una app de Lovable o Bolt sin ajustes adicionales?

Sin ajustes, no. Estos builders priorizan velocidad de iteración sobre seguridad por defecto. Sin revisar variables de entorno, RLS de Supabase, headers de seguridad y exposición de claves en el bundle, vas a estar publicando una app con vulnerabilidades triviales de explotar.

¿Cuánto tiempo lleva asegurar una app vibe-coded antes del launch?

Entre 2 y 6 horas si seguís este checklist y nunca hiciste el proceso. Es mucho más rápido si automatizás con una auditoría como VibeAuditt para identificar qué falta, y luego corregís solo lo necesario. El monitoring semanal te alerta cuando un cambio rompe algo.

¿Lovable y Bolt me protegen de XSS automáticamente?

Parcialmente. Frameworks modernos como React escapan strings por defecto, eliminando XSS reflejado en la mayoría de casos. Pero si usás `dangerouslySetInnerHTML` o `innerHTML` directamente (que estos builders sugieren a veces para flexibilidad), abrís el vector. Y sin CSP, un XSS exitoso puede hacer lo que quiera.

¿Qué hago si ya publiqué con vulnerabilidades?

Primero auditá. Si tenés claves expuestas, rotalas inmediatamente (dashboard de Stripe / OpenAI / Supabase). Si tu Supabase no tiene RLS, deshabilitá el acceso público hasta que apliques políticas. Si tenés .git/ expuesto, configurá tu hosting para 404 en ese path. Luego seguí el resto del checklist.

¿Tengo que configurar todos los headers de seguridad manualmente?

Sí, los builders no los configuran por default. En Cloudflare Pages podés usar `_headers`, en Vercel `vercel.json` con `headers:`, en Netlify `netlify.toml`. Es config simple pero requiere 15 minutos. Sin headers, perdés la primera línea de defensa contra XSS, clickjacking y MITM.

// escanear ahora

Audita tu SaaS gratis

Recibí un reporte de seguridad en menos de un minuto. Detectamos claves expuestas, RLS deshabilitado, headers de seguridad ausentes y más. Sin tarjeta, sin instalación.

Empezar auditoría

10 auditorías gratis al mes · reportes detallados desde 9 USD