// docs / security guides / hardening
Cómo asegurar una app creada con herramientas de IA
Guía de hardening paso a paso para apps que creaste con Cursor, Claude Code, Lovable, Bolt, v0, Replit o Windsurf. Cuatro fases: entender por qué las apps generadas por IA fallan distinto, ejecutar una auditoría inmediata del código, hacer hardening en el deploy y mantener el monitoreo. Opinionada, narrativa, con fragmentos reales que puedes copiar.
Por qué las aplicaciones generadas por AI- fallan de manera diferente
Las aplicaciones codificadas por Vibe pueden ser seguras. Necesitan un pase de auditoría adicional porque los modos de falla son estructurales, no descuidados:
- Assistants inline hardcoded keys. You ask for a fix to an auth error and get a pasted Supabase example that assumes a service-role client. The key ends up at the top of a page component. Both the anon client and the service client coexist; both ship.
- Generated servers default to permissive CORS. Generated Express / Fastify handlers often ship with
cors({ origin: '*' })because that's the fastest way to get a working preview. The middleware never gets a second pass. - Rules files get skipped. Firestore-backed projects generate the data model but rarely touch
firestore.rules. Test-mode rules let anyone read and overwrite data until someone replaces them. - RLS never enters the migration. A generated Supabase schema and CRUD surface use the anon key, but
ENABLE ROW LEVEL SECURITYnever enters the migration. Anonymous users can read or write any row. - Handlers trust IDs. A generated
GET /api/items/[id]reads the param and queries Postgres without verifying ownership. Active scans on a verified domain test for this (IDOR / BOLA).
La auditoría inmediata: busque en su código base patrones de riesgo
Antes de endurecer algo, encuentra lo que ya está roto. Cada uno de estos greps tarda menos de un minuto:
Secretos y claves de proveedor
grep -RIn 'NEXT_PUBLIC_SUPABASE_SERVICE' src/
grep -RIn 'sk_live_\|pk_live_\|STRIPE_SECRET' src/
grep -RIn 'sk-ant-\|^sk-' src/ # Anthropic / OpenAI
grep -RIn 'AIza\|AKIA' src/ # Google / AWS
grep -RIn 'eyJh[A-Za-z0-9_-]\{20,\}' src/ # JWT-shaped stringsCualquier hit debe eliminarse y rotarse la clave. Provider paneles de control: Supabase → Configuración → API, Stripe → Desarrolladores → API claves, consola Anthropic / OpenAI.
Controles de acceso a la base de datos
# Supabase migrations
grep -RIn 'CREATE TABLE public\.' supabase/migrations/
grep -RIn 'ENABLE ROW LEVEL SECURITY\|FORCE ROW LEVEL SECURITY' supabase/migrations/
# Firebase / Firestore
cat firestore.rules # confirm no `if true;` matchesCada CREATE TABLE public.* necesita un ENABLE ROW LEVEL SECURITY coincidente y al menos una política. Las reglas de Firestore deben limitar las lecturas a request.auth.uid.
Manejo de autenticación y sesión
grep -RIn 'getSession()' src/ # should be getUser() server-side
grep -RIn 'localStorage\.\(set\|get\)Item.*token' src/
grep -RIn 'jwt.verify.*\(noVerify\|skipVerify\)' src/Las rutas renderizadas por el servidor deben usar supabase.auth.getUser(); se verifica con el backend. getSession() lee una cookie no verificada. Los tokens en localStorage son accesibles para cualquier script que se ejecute en la página.
Encabezados y middleware
# Confirm middleware location for src/ layouts
ls src/middleware.ts middleware.ts 2>&1
# Look for CSP and security headers
grep -RIn 'Content-Security-Policy\|Strict-Transport-Security' src/Con el diseño src/, solo se selecciona src/middleware.ts. Si su archivo de middleware está en la raíz del proyecto, Next.js lo ignora silenciosamente y su lógica CSP/auth-refresh nunca se ejecuta.
Endurecimiento en el momento de la implementación
Una vez que la fuente esté limpia, bloquee cómo la aplicación llega a producción.
Paso 1: ambientes separados
Vercel: tres entornos: Producción (su dominio de producción), Vista previa (PR / implementaciones provisionales), Desarrollo (local). Cada uno tiene su propio conjunto env-var. Las teclas Live Stripe / Anthropic / Supabase nunca llegan a la Vista previa; Las claves de vista previa nunca llegan a Producción. Las ramas pasan a Vista previa automáticamente; fusionarse con main se implementa en Producción.
Paso 2: Estricto CSP mediante middleware
Genere un nonce por solicitud y luego inyéctelo en Content-Security-Policy. Next.js aplica automáticamente el nonce a sus propias etiquetas de script cuando configura el encabezado de solicitud x-nonce.
// src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = crypto.randomUUID().replace(/-/g, '');
const csp = [
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`connect-src 'self' https://*.supabase.co`,
`object-src 'none'`,
`base-uri 'self'`,
`frame-ancestors 'none'`,
].join('; ');
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set('Content-Security-Policy', csp);
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
return response;
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};Paso 3: Fuerza RLS en cada mesa pública
RLS isn't enabled by default on tables you create in SQL or migrations. Enable it on every exposed table and pair each one with explicit policies per role — that is what stops the anon and authenticated roles. FORCE only makes the table owner obey RLS too.
-- supabase/migrations/XXXX_rls.sql
alter table public.profiles enable row level security;
alter table public.profiles force row level security;
create policy "profiles: read own"
on public.profiles for select
using (auth.uid() = id);
create policy "profiles: update own"
on public.profiles for update
using (auth.uid() = id)
with check (auth.uid() = id);Paso 4: Verificación de autenticación solo del servidor en cada ruta API
Cada ruta API que cambia de estado verifica el lado del servidor de la persona que llama con supabase.auth.getUser(). El objeto de usuario se convierte en la fuente de verdad para user_id; nunca confíe en el cuerpo de una solicitud para configurarlo.
// src/app/api/items/route.ts
import { NextResponse, type NextRequest } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function POST(request: NextRequest) {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
const body = await request.json();
const { data, error } = await supabase
.from('items')
.insert({ ...body, user_id: user.id }) // server-supplied, not from body
.select()
.single();
if (error) return NextResponse.json({ error: error.message }, { status: 400 });
return NextResponse.json(data);
}Paso 5: realice un proxy inverso de sus análisis
Proxying análisis a través de su propio dominio evita bloqueadores de anuncios y permite que su CSP connect-src 'self' se mantenga limitado. El mismo patrón funciona para PostHog, Plausible, Umami y receptores de eventos personalizados.
// src/app/api/posthog/[...path]/route.ts
import { type NextRequest } from 'next/server';
const UPSTREAM = 'https://us.i.posthog.com';
export async function POST(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path } = await params;
const url = `${UPSTREAM}/${path.join('/')}`;
return fetch(url, {
method: 'POST',
headers: { 'content-type': req.headers.get('content-type') ?? 'application/json' },
body: await req.text(),
});
}Paso 6: guardia de redireccionamiento abierto en el rebote posterior a la autenticación
Los flujos de inicio de sesión/registro suelen aceptar un parámetro de consulta next. Rechace todo lo que no sea una ruta del mismo sitio: comience con / y nunca con // (relativo al protocolo, envía a los usuarios fuera del sitio).
function safeNext(raw: string | null): string {
if (!raw) return '/dashboard';
if (!raw.startsWith('/') || raw.startsWith('//')) return '/dashboard';
return raw;
}En curso: monitoreo y reescaneo
La deriva ocurre en cada despliegue. Trate la seguridad como un bucle, no como una lista de verificación que debe completar.
Verifique su dominio de producción
Dashboard → Domains → add your production domain → DNS TXT or HTTP-file verification. Active scans require Hobby or above; scheduled re-scans require Pro or Unlimited.
Programar nuevos análisis pasivos
Scheduled re-scans are available on Pro and Unlimited for verified domains. Free and Hobby scans are manual. Scheduled scans use your plan allowance. Configure completion email preferences and a scan.completed webhook if needed.
# Or from CI, via the REST API:
curl -X POST https://fixvibe.app/api/v1/scans \
-H "authorization: Bearer $FIXVIBE_TOKEN" \
-H "content-type: application/json" \
-d '{"target":"https://your-app.com"}'Habilitar API-análisis activos (opcional)
Si desea un sondeo activo automatizado (SQLi / XSS / IDOR caminando / etc.), actívelo por dominio en Panel → Dominios → API activo. La autorización es duradera, tiene un vencimiento de 90 días y es revocable instantáneamente. Emparéjelo con el webhook scan.active_api.first_used para que el primer escaneo activo automatizado después de la habilitación llegue a su alerta.
Conecte los hallazgos a su flujo de trabajo AI
On Hobby or above, create an API token at Account → API tokens and configure the MCP server (/docs/mcp) in your coding tool. Ask your agent to run an authorized scan and inspect the highest-severity findings. Code fixes can use remediation prompts; provider and DNS fixes may need manual operator steps.
Detección de amenazas en vivo (Unlimited)
Periodic certificate-transparency, DNS, JS-bundle, and threat-intelligence checks report observed changes on supported signals. Alerts depend on successful polling and source availability; they do not establish continuous or complete security coverage.
Patrones de fallas reales y sus soluciones.
Five common patterns in AI-generated apps, each with the actual fix:
- Clave de función de servicio en un componente de cliente
Symptom: FixVibe reports an exposed Supabase service-role key on the production URL. Cause: an autocomplete pasted
createClient(URL, SERVICE_ROLE_KEY)into a React component. Fix: move the service client tosrc/lib/supabase/service.tswithimport 'server-only'at the top; create a parallelsrc/lib/supabase/client.tsusing the anon key for client-side use; rotate the service-role key via Supabase Studio. - Reglas de Firestore dejadas en modo de prueba
Symptom: a high-severity open Firebase rules finding. Cause: generated rules read
allow read, write: if request.time < timestamp.date(2026, 6, 1);— a time-bounded "allow all". Fix: scope each rule to the authenticated user —match /users/{userId}/posts/{postId} { allow read, write: if request.auth.uid == userId; }— and re-deployfirebase deploy --only firestore:rules. - Permisivo CORS sobreviviendo en producción
Symptom: a high-severity CORS misconfiguration finding. Cause: generated Express middleware:
app.use(cors({ origin: '*' })). Fix: allowlist your frontend origin:app.use(cors({ origin: ['https://your-app.com'], credentials: true })). For Next.js API routes, setAccess-Control-Allow-Originexplicitly in the response. - RLS habilitado pero no forzado
Symptom: FixVibe reports that anonymous visitors can read a public table even though RLS looks enabled in the dashboard. Cause: RLS is on, but a policy such as
USING (true)lets theanonrole through, or the migration that tightened it never ran in production. Fix: replace the permissive policy with one scoped toauth.uid(), apply the migration, and re-scan. - Identificaciones transitables IDOR- sin firmar
Symptom: an active scan on your verified domain reports that one user can read another user's records at
/api/items/1,/api/items/2, ... Cause: the API handler trusts the path param and queries Postgres without an ownership predicate. Fix: add.eq('user_id', user.id)on every read query, or move to signed URLs / UUIDs scoped under/api/users/[uid]/items/[id].
El bucle de seguridad del código de vibración
El objetivo no es la seguridad perfecta; está eliminando la fruta más fácil que las herramientas AI pasan por alto constantemente para que puedas seguir realizando envíos rápidos.
- Generate fast — utilice Cursor, Claude Code, Lovable, Bolt. Ese es el punto.
- Audit immediately: ejecute el grep configurado anteriormente, verifique RLS, verifique CSP, revise el límite de autenticación.
- Harden at deploy: middleware, separación de entornos, CSP nonce, HSTS, verificación de autenticación solo del servidor.
- Monitor — FixVibe pasivo diario, activo semanalmente en un dominio verificado, webhooks para Slack, detección de amenazas en Unlimited.
- Fix fast — use FixVibe coding-agent prompts for code/config findings and operator steps for DNS, provider, secret-rotation, or manual-review findings. Re-deploy, re-scan, close the loop.
Próximos pasos
Para conocer el contexto conceptual sobre DAST frente a SAST y por qué las aplicaciones generadas por AI- necesitan su propio escaneo, lea AI-generated code security scanning. Para obtener una referencia rápida sobre la auditoría previa al envío, consulte vibe coding security checklist.
