10 verificações críticas que apps Lovable em produção quase sempre falham — env leaks, RLS Supabase, CORS, auth patterns e como consertar cada uma.
Lovable ficou popular porque entrega app funcionando em minutos. Sobe no Vercel, usa Supabase, gera Tailwind bonito, autentica com magic link. Pra prototipar, é imbatível.
Só que o Lovable — como qualquer ferramenta de vibe coding — otimiza para "funciona" e não para "está seguro". Cada projeto Lovable que a gente auditou nos últimos 6 meses falhou em pelo menos 4 desses 10 checks. A maioria falhou em 7.
Se você tem app Lovable em produção com usuário real, especialmente com dado pessoal (LGPD), leia isso hoje. Este texto assume que você conhece Supabase minimamente. Se ainda não colocou em produção, leia primeiro o artigo de vibe coding vulnerabilities para contexto amplo.
1. Chaves publishable vs. service_role — o clássico
Todo projeto Supabase tem duas chaves. anon (ou publishable) é pública, vai no cliente. service_role é senha mestra, bypass total de RLS, deve viver só no servidor.
Lovable às vezes gera código que expõe service_role no cliente. Sintomas:
- Arquivo
.env(ou.env.local) commitado no repo comSUPABASE_SERVICE_ROLE_KEY. - Chamada
createClient(url, serviceRoleKey)em componente React. - Uso de service_role em
useEffectou hook client-side.
Como testar:
# Baixa o bundle da produção
curl -s https://seuapp.vercel.app/assets/index.js | grep -oE "eyJhbGci[A-Za-z0-9._-]{50,}"
Se aparecer um JWT longo, decode em jwt.io. Se o role for service_role, sua aplicação inteira está aberta. Emergency: rotacione a chave no dashboard Supabase agora, refaça deploy com anon.
2. RLS ausente ou permissivo demais
Row Level Security é o mecanismo do Supabase para autorização por linha. Sem RLS, qualquer usuário com anon key faz select * from users e leva todo mundo. Isso é a vulnerabilidade nº 1 de projetos Lovable/Supabase.
Como testar:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('SUA_URL', 'SUA_ANON_KEY')
// Sem login. Se retornar linhas de outros usuários, RLS está quebrada.
const { data, error } = await supabase.from('users').select('*')
console.log(data)
Se retorna dados de qualquer usuário, você tem problema.
Correção:
-- Habilita RLS
alter table users enable row level security;
-- Policy típica: usuário só vê próprio registro
create policy "users_select_own" on users
for select using (auth.uid() = id);
create policy "users_update_own" on users
for update using (auth.uid() = id);
Repita para toda tabela sensível. Cuidado com policies muito permissivas do tipo using (true) — Lovable gera essas com frequência quando o desenvolvedor pede "faz funcionar".
3. Edge Functions sem validação de auth
Edge Functions do Supabase são endpoints serverless. Se você não valida o JWT do usuário, qualquer um chama.
Padrão ruim (comum em Lovable):
Deno.serve(async (req) => {
const { userId, amount } = await req.json()
// Faz operação usando userId direto do body — vulnerável.
await supabase.from('transactions').insert({ user_id: userId, amount })
return new Response('ok')
})
Padrão correto:
Deno.serve(async (req) => {
const authHeader = req.headers.get('Authorization')
if (!authHeader) return new Response('Unauthorized', { status: 401 })
const supabase = createClient(url, anonKey, {
global: { headers: { Authorization: authHeader } }
})
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) return new Response('Unauthorized', { status: 401 })
const { amount } = await req.json()
// Use user.id, NUNCA userId vindo do body
await supabase.from('transactions').insert({ user_id: user.id, amount })
return new Response('ok')
})
4. CORS aberto (*)
Lovable, para "funcionar", frequentemente configura CORS liberado. Em Edge Function:
// Errado
return new Response(body, {
headers: { 'Access-Control-Allow-Origin': '*' }
})
Com CORS wildcard + credenciais, você abriu para qualquer origem chamar sua API autenticada. Fixe para domínio específico:
return new Response(body, {
headers: {
'Access-Control-Allow-Origin': 'https://seuapp.vercel.app',
'Access-Control-Allow-Credentials': 'true'
}
})
5. Magic link sem rate limit
O fluxo de magic link do Supabase, por padrão, permite envio ilimitado por email/hora. Alguém pode encher a caixa de entrada de qualquer usuário seu, ou usar seu domínio para spam via bounces.
Como resolver: no dashboard Supabase, Authentication → Rate Limits. Configure Email OTP para no máximo 3 tentativas/hora por email. E adicione captcha (Turnstile/hCaptcha) no formulário de signup e signin.
6. Storage bucket público sem necessidade
Supabase Storage tem buckets públicos e privados. Lovable frequentemente cria bucket público "porque é mais fácil". Se você guarda documento, foto de perfil, upload de usuário — provavelmente devia ser privado com URL assinada.
Como testar: liste seus buckets no dashboard. Para cada público, pense: "se alguém enumerar UUIDs de arquivos, ele acessa algo sensível?".
Correção:
update storage.buckets set public = false where id = 'user-uploads';
E policy:
create policy "users can read own files" on storage.objects
for select using (
bucket_id = 'user-uploads'
and auth.uid()::text = (storage.foldername(name))[1]
);
7. .env commitado ou vazando no bundle Vercel
Lovable escreve .env automaticamente. Vercel só considera variável se prefixo NEXT_PUBLIC_ ou similar for consciente. O erro comum é o desenvolvedor colocar chave sensível com prefixo público — Vite mostra no bundle.
Como testar:
# Baixa o bundle
curl -s https://seuapp.vercel.app/ | grep -oE 'src="[^"]*\.js"' | head -5
# Baixa cada JS
curl -s https://seuapp.vercel.app/assets/index-abc.js | grep -oE '(sk_live|sk_test|SG\.|xoxb-|AIza|eyJhbGci)[A-Za-z0-9_-]+'
Qualquer match aqui é vazamento. Rotacione a chave.
8. Ausência de webhook signature validation
Se seu app Lovable recebe webhook de Stripe, MP, ClickSign, Iugu — precisa validar assinatura. Padrão preguiçoso comum:
Deno.serve(async (req) => {
const event = await req.json() // Aceita qualquer JSON
if (event.type === 'checkout.session.completed') {
// Marca pedido como pago sem checar signature
}
})
Isso é forjável em segundos. Qualquer um envia POST com JSON falso e ganha o produto de graça.
Padrão correto (Stripe):
const signature = req.headers.get('stripe-signature')
const body = await req.text()
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
// Só agora confia no event
9. Ausência de MFA para admin
App tem tabela users com coluna is_admin = true? Como você loga como admin — mesma senha, mesmo fluxo, mesmo magic link? Sem MFA?
Configure Supabase para exigir MFA em usuários com role admin. É trabalho de meia hora e evita a categoria de incidente onde phishing pega o email do admin.
10. Sem headers de segurança no Vercel
Vercel serve seu app sem headers de segurança por default. Adicione vercel.json:
{
"headers": [{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" }
]
}]
}
Teste em securityheaders.com. Se sua nota é F, corrija hoje.
Roteiro de correção sugerido
Se você tem 4 horas em um sábado:
- Rode os testes das seções 1, 2, 7 primeiro (chaves e RLS). Isso é onde vaza dado.
- Corrija 3, 4, 8 (auth em Edge Function, CORS, webhook signature). Isso é onde tem RCE de negócio.
- Ajuste 5, 6, 9, 10 (rate limit, storage, MFA, headers). Higiene.
Depois, ligue varredura contínua para não voltar a acumular esse tipo de coisa quando você (ou o Lovable) fizer o próximo deploy.
A Varredura testa RLS pública, chaves vazadas, headers e CORS em apps Lovable/Supabase automaticamente, sem plugin, sem token, sem código. Rode agora.
Sua app em produção passa por essa análise?
Rode uma varredura autônoma em ~60 segundos. Sem cadastro.