Guia técnico de segurança para apps Bolt.new em produção — riscos do WebContainer, padrões Node que o Bolt emite e o que corrigir antes de escalar.
Bolt.new pega prompt em linguagem natural e devolve app Node/Vite rodando dentro de WebContainer no navegador. Excelente para prototipar. Terrível para colocar em produção sem revisar.
O jeito que o Bolt emite código tem padrões específicos — diferentes do Lovable, do Cursor Agent, do v0. Este guia é sobre os padrões inseguros que o Bolt tende a produzir por default e o que corrigir antes de ter cliente pagante.
Para contexto amplo de vibe coding, veja 12 vibe coding vulnerabilities. Este texto assume que você já leu.
O contexto Bolt: WebContainer, Node moderno, deploys apressados
Bolt roda em WebContainer (StackBlitz). É um Node isolado no browser via WASM. Isso muda algumas coisas em relação ao Lovable (que costuma gerar frontend + Supabase) e ao Replit (que roda em VM Linux tradicional):
- Bolt tende a emitir aplicações Node fullstack (Express, Hono, Fastify) com SQLite ou Postgres via Prisma.
- Deploy típico é Netlify, Cloudflare Workers ou Vercel — não StackBlitz em produção.
- Gerenciamento de secrets é frequentemente via
.envque o Bolt cria sozinho e o desenvolvedor não revisa.
1. Endpoint /api sem autenticação por default
O Bolt, quando você pede "crie um app de tarefas", frequentemente gera Express com rotas assim:
app.get('/api/tasks', async (req, res) => {
const tasks = await prisma.task.findMany()
res.json(tasks)
})
app.post('/api/tasks', async (req, res) => {
const task = await prisma.task.create({ data: req.body })
res.json(task)
})
Nenhum middleware de auth. Nenhum filtro por usuário. Roda perfeito na demo — vaza tudo em produção.
Correção mínima:
import { requireAuth } from './middleware/auth.js'
app.use('/api', requireAuth)
app.get('/api/tasks', async (req, res) => {
const tasks = await prisma.task.findMany({
where: { userId: req.user.id } // filtra por dono
})
res.json(tasks)
})
O middleware requireAuth valida JWT ou session cookie. Bolt raramente gera isso automaticamente.
2. Prisma sem validação de input
Prisma protege contra SQL injection clássico. Não protege contra query malformada por payload malicioso. Padrão comum do Bolt:
app.post('/api/search', async (req, res) => {
const results = await prisma.user.findMany({
where: req.body.filter // Aceita qualquer filtro do cliente
})
res.json(results)
})
Cliente envia { "filter": { "email": { "contains": "@" } } } e leva a lista inteira de emails. Ou pior, { "filter": {} } e leva tudo. Ou { "filter": { "AND": [...consulta arbitrária...] } } explorando relações.
Correção: valide o schema do input com zod antes de passar para Prisma.
import { z } from 'zod'
const searchSchema = z.object({
name: z.string().min(1).max(50).optional()
})
app.post('/api/search', requireAuth, async (req, res) => {
const parsed = searchSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'invalid input' })
const results = await prisma.user.findMany({
where: {
companyId: req.user.companyId,
name: parsed.data.name ? { contains: parsed.data.name } : undefined
},
select: { id: true, name: true, email: false } // whitelist explícita
})
res.json(results)
})
3. CORS liberado geral
Bolt frequentemente adiciona:
import cors from 'cors'
app.use(cors()) // Aceita qualquer origem
Ou pior:
app.use(cors({ origin: '*', credentials: true }))
Com credentials: true, isso nem é aceito por browser moderno — mas o desenvolvedor não sabe e pensa que está funcionando. Configure explicitamente:
app.use(cors({
origin: ['https://seuapp.com', 'https://www.seuapp.com'],
credentials: true
}))
4. JWT com secret fraco ou hardcoded
Padrão que a gente encontra frequente em Bolt:
const token = jwt.sign({ userId: user.id }, 'supersecret123')
O secret está commitado. Qualquer um que acha o repo GitHub forja token de admin.
Correção:
// .env
JWT_SECRET=<32+ bytes randômicos, gerados com openssl rand -hex 32>
// código
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '15m', issuer: 'seuapp' }
)
E renove com refresh token separado. expiresIn: '7d' (padrão bolt) é longo demais.
5. Ausência de bcrypt/argon2
Bolt às vezes gera:
if (user.password === req.body.password) { /* login */ }
Comparação de texto puro. Senha no banco em plaintext. Não é hipótese, é código real que vimos.
Correção urgente:
import bcrypt from 'bcrypt'
// signup
const hash = await bcrypt.hash(req.body.password, 12)
await prisma.user.create({ data: { email, password: hash } })
// signin
const valid = await bcrypt.compare(req.body.password, user.password)
if (!valid) return res.status(401).json({ error: 'invalid credentials' })
Se você tem app Bolt em prod com plaintext, force reset de senha em todos usuários assim que corrigir.
6. SQLite em produção
Bolt ama gerar app com SQLite via Prisma (file:./dev.db). Bom para dev. Ruim para prod porque:
- Bloqueia escrita concorrente (row locking).
- Não roda em serverless/Vercel (filesystem efêmero).
- Backup não é trivial.
Se seu Bolt exportou schema.prisma com provider sqlite, mude para postgres antes do primeiro cliente pagante:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Neon, Supabase Postgres, Railway Postgres — qualquer um serve.
7. Sem rate limit em endpoint público
Bolt gera signup público sem rate limit. Alguém escreve script e cria 10 mil usuários em 5 minutos. Você fica com base de dados suja e conta de email service explodindo.
Correção:
import rateLimit from 'express-rate-limit'
const signupLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1h
max: 5, // 5 signups por IP por hora
message: 'muitas tentativas, tente mais tarde'
})
app.post('/api/signup', signupLimiter, async (req, res) => { /* ... */ })
8. Endpoint /health vazando informação
Bolt tende a adicionar:
app.get('/health', (req, res) => {
res.json({
status: 'ok',
node: process.version,
env: process.env.NODE_ENV,
db: process.env.DATABASE_URL, // Ops.
uptime: process.uptime()
})
})
Retornar DATABASE_URL é vazamento de credencial. Mesmo sem isso, retornar versão de Node ajuda atacante a pesquisar CVE específico. Simplifique:
app.get('/health', (req, res) => res.json({ status: 'ok' }))
9. Erros expondo stack trace
Padrão bolt de erro handler:
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message, stack: err.stack })
})
Stack trace mostra caminho de arquivo, linha, dependência. Ouro para atacante mapear a aplicação.
Correção:
app.use((err, req, res, next) => {
console.error(err) // loga internamente
res.status(500).json({
error: process.env.NODE_ENV === 'production' ? 'internal error' : err.message
})
})
10. Headers de segurança ausentes
Bolt Express sem helmet. Adicione:
import helmet from 'helmet'
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"]
}
}
}))
Teste em securityheaders.com. Alvo: A ou A+.
Fluxo de revisão sugerido
Se você acabou de subir app Bolt em produção:
- Primeiros 15 min: rode
git log -pe procure secrets. Rotacione o que achar. - Próximos 30 min: revise rotas
/api/*— todas têm auth? Todas filtram por dono? - Próximas 2 horas: aplique correções 4, 5, 6, 9, 10 (JWT, bcrypt, DB, error handler, helmet).
- Próximo dia: rate limit, CORS restrito, health simplificado.
- Próxima semana: ligue varredura contínua para não repetir o ciclo no próximo prompt "adiciona funcionalidade X".
Depois disso, veja também inventário de exposição em 1 hora — é possível que seu Bolt tenha ficado ligado em outros lugares (subdomínios de preview do Netlify, deploys antigos) que você esqueceu.
A Varredura testa apps Node/Express gerados por IA sem tocar em código — encontra rotas sem auth, IDOR, CORS aberto, headers ausentes, secrets vazados no bundle. Rode agora.
Sua app em produção passa por essa análise?
Rode uma varredura autônoma em ~60 segundos. Sem cadastro.