Saltar al contenido
Checklist de seguridad antes de lanzar tu app hecha con IA
ChatGPT y modelos

Checklist de seguridad antes de lanzar tu app hecha con IA

Antes de lanzar una app hecha con IA, revisa 8 puntos: claves solo en el servidor, límites de uso, autenticación en cada ruta, webhooks de pago con firma verificada, copias restauradas, registros sin datos personales, una prueba de punta a punta con pago de prueba y una revisión final. Cada punto incluye un ejemplo y una forma de comprobarlo.

Por · Equipo de contenido y revisiónPublicado: 16 min de lectura

Puntos clave

Los puntos que más importan

  • Una clave en el frontend o en el historial de git ya no es secreta: rótala y mueve la llamada al servidor.
  • Pon un tope de gasto en el proveedor de IA y un límite por usuario en tu endpoint.
  • Stripe y MercadoPago firman sus webhooks: verifica la firma, confirma el pago en la API y haz el proceso idempotente.
  • Una prueba de punta a punta con pago de prueba detecta fallos que las pruebas unitarias no ven.

Hacer que una app funcione en tu computadora con ayuda de IA ya es cuestión de una tarde. Lo que casi nadie revisa es lo que pasa cuando entra el primer usuario real, el primer bot o el primer pago. Las apps hechas con asistentes de código suelen fallar en los mismos lugares, y casi siempre se trata de dinero o de datos: una clave que queda a la vista, un endpoint sin límite, una ruta sin autenticación o un pago que se marca como aprobado sin confirmarlo.

Esta lista tiene 8 controles. Cada uno se puede revisar en menos de una hora, tiene un ejemplo de código o un comando y una forma de comprobar que quedó bien. Si cobras con MercadoPago o Stripe, el punto 4 es el que más importa.

Cómo armamos esta lista

Partimos de una pregunta: ¿qué errores de una app recién lanzada cuestan dinero o exponen datos, y se pueden corregir antes de lanzar sin ser experto en seguridad? Descartamos lo que exige auditorías formales (pentest, certificaciones, cumplimiento normativo) y nos quedamos con controles verificables.

Las reglas de cada punto salen de documentación oficial, no de opiniones:

  • La guía de seguridad de claves de OpenAI, para el manejo de claves y los límites de gasto.
  • La documentación de webhooks de Stripe y de MercadoPago, para la verificación de firmas y los reintentos.
  • La documentación de secret scanning de GitHub, para saber qué hacer cuando una clave llegó a un repositorio.

Los fragmentos de código son ejemplos mínimos para entender la idea. Adáptalos a tu framework y pruébalos antes de confiar en ellos.

1. Claves secretas: solo en el servidor

Una clave de API es como una tarjeta de crédito: quien la tiene gasta a tu nombre. OpenAI lo dice sin rodeos en su guía de seguridad: nunca despliegues la clave en entornos de cliente como navegadores o apps móviles, porque cualquiera puede tomarla y hacer solicitudes en tu nombre; las solicitudes deben pasar por tu propio servidor.

Un asistente de IA a veces pone la clave en el frontend "para que funcione rápido". Revisa tres cosas:

  1. Variables públicas. En Next.js, todo lo que empieza con NEXT_PUBLIC_ termina dentro del código que descarga el navegador; en Vite pasa lo mismo con VITE_. Una clave secreta nunca debe llevar esos prefijos.
  2. El build del frontend. Compila el proyecto y busca tus claves en la carpeta que se publica:
npm run build
grep -r "sk-" .next/static dist 2>/dev/null | head

Si aparece algo, esa clave ya no es secreta.

  1. El historial de git. Borrar el archivo .env hoy no borra lo que se subió ayer:
git grep -nIE "(sk-[A-Za-z0-9_-]{20,}|whsec_|APP_USR-)" $(git rev-list --all) | head

El patrón sk- es el formato de las claves de OpenAI, whsec_ el de los secretos de webhook de Stripe y APP_USR- el de las credenciales de producción de MercadoPago. Ajusta el patrón a los proveedores que uses.

La estructura correcta es que el navegador llame a tu endpoint y que tu servidor llame al proveedor:

// app/api/resumir/route.js (se ejecuta solo en el servidor)
export async function POST(req) {
  const { texto } = await req.json();
  if (typeof texto !== "string" || texto.length > 4000) {
    return Response.json({ error: "Texto inválido" }, { status: 400 });
  }
  const resultado = await llamarAlModelo(texto); // usa process.env.OPENAI_API_KEY
  return Response.json({ resumen: resultado });
}

Si una clave llegó a git, rótala. GitHub lo recomienda en su documentación de secret scanning: cuando hay una alerta, rota la credencial de inmediato; limpiar el historial lleva tiempo y suele ser innecesario si ya revocaste la clave. Crea la clave nueva, actualiza tu app, comprueba que funciona y revoca la vieja. Aprovecha para crear claves con fecha de vencimiento si el proveedor lo permite, como hace OpenAI con las claves de proyecto.

2. Límite de uso: que nadie te vacíe la cuenta en una noche

Si tu endpoint llama a un modelo de pago, cada solicitud cuesta dinero. Sin límites, un usuario curioso o un bot puede generar una factura enorme. Pon límites en tres niveles:

  • En el proveedor: configura un tope mensual de gasto. OpenAI recomienda definir límites mensuales y activar el límite estricto de gasto (hard limit), porque las alertas solas no detienen el tráfico. La propia guía advierte que el bloqueo puede rechazar solicitudes legítimas y que el gasto puede pasarse un poco del tope.
  • En tu endpoint: limita las solicitudes por usuario (mejor que por IP, que se comparte) y el tamaño de cada entrada.
  • En la llamada al modelo: fija un máximo de tokens de salida y no dejes que el usuario elija el modelo.

Un limitador mínimo en memoria, útil para una sola instancia:

const VENTANA_MS = 60_000;
const MAXIMO = 10;
const registro = new Map();

export function permitir(clave) {
  const ahora = Date.now();
  const recientes = (registro.get(clave) ?? []).filter((t) => ahora - t < VENTANA_MS);
  if (recientes.length >= MAXIMO) {
    registro.set(clave, recientes);
    return false;
  }
  recientes.push(ahora);
  registro.set(clave, recientes);
  return true;
}

Úsalo así: if (!permitir(usuario.id)) return Response.json({ error: "Demasiadas solicitudes" }, { status: 429 });

Este limitador vive en la memoria del proceso. Si tu app corre en varias instancias o en funciones serverless, cada una tendrá su propio contador; en ese caso guarda los contadores en un almacén compartido como Redis.

Cómo comprobarlo: lanza 20 solicitudes seguidas con un script y confirma que las últimas devuelven 429 y que tu panel de gasto no se mueve.

3. Autenticación en cada ruta, no solo en la pantalla

Ocultar un botón no protege nada. Quien conozca la URL de tu API puede llamarla directamente. Los asistentes de IA suelen proteger las pantallas y olvidar los endpoints. Haz un inventario: lista todas las rutas de tu carpeta api y marca cuáles exigen sesión.

El segundo error es verificar que el usuario inició sesión pero no que el recurso le pertenece. Si /api/facturas/123 devuelve la factura sin comprobar de quién es, cualquiera puede probar 124, 125, 126.

const sesion = await obtenerSesion(req);
if (!sesion) return Response.json({ error: "No autenticado" }, { status: 401 });

const factura = await db.query(
  "select * from facturas where id = $1 and usuario_id = $2",
  [params.id, sesion.usuarioId]
);
if (factura.rows.length === 0) return Response.json({ error: "No encontrada" }, { status: 404 });

Observa que la consulta filtra por usuario_id. Así, una factura ajena y una inexistente producen la misma respuesta.

Cómo comprobarlo: crea dos usuarios de prueba. Con el usuario A copia el identificador de uno de sus recursos e intenta abrirlo con la sesión del usuario B. Debe fallar. Repite sin sesión: también debe fallar.

4. Webhooks de pago: verifica la firma y confirma el pago

Un webhook es una solicitud que el proveedor de pagos envía a tu servidor cuando algo ocurre. Si tu endpoint acepta cualquier solicitud, un atacante puede enviarte un "pago aprobado" falso y recibir su producto gratis.

Reglas comunes a Stripe y MercadoPago:

  • Verifica la firma con el secreto de webhook de tu endpoint o aplicación antes de procesar el evento.
  • No uses la redirección del navegador como prueba de pago. Que el usuario vuelva a una URL de "gracias" no significa que pagó. El estado real lo confirma el webhook y la consulta a la API del proveedor.
  • Responde rápido con 2xx y procesa el trabajo pesado aparte.
  • Haz el proceso idempotente. El mismo evento puede llegar más de una vez; guarda el identificador del evento y no entregues dos veces.

Stripe

Stripe firma cada evento en el encabezado Stripe-Signature. Su documentación indica que debes verificarlo con constructEvent, pasando el cuerpo exacto de la solicitud, el encabezado y el secreto del endpoint, que empieza con whsec_. Si algún framework modifica el cuerpo antes de que llegue a tu código (reordena claves, cambia espacios), la verificación falla. Por eso hay que leer el cuerpo en bruto:

// app/api/webhooks/stripe/route.js
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function POST(req) {
  const cuerpo = await req.text(); // cuerpo en bruto, sin JSON.parse
  const firma = req.headers.get("stripe-signature");
  let evento;
  try {
    evento = stripe.webhooks.constructEvent(cuerpo, firma, process.env.STRIPE_WEBHOOK_SECRET);
  } catch {
    return new Response("Firma inválida", { status: 400 });
  }

  if (await eventoYaProcesado(evento.id)) return new Response("ok", { status: 200 });

  try {
    switch (evento.type) {
      case "checkout.session.completed":
      case "checkout.session.async_payment_succeeded": {
        const sesion = evento.data.object;
        // Con tarjeta, el pago ya viene confirmado ("paid"). Con métodos diferidos
        // (transferencia, débito bancario) llega "unpaid": no entregues todavía.
        if (sesion.payment_status === "paid") await cumplirPedido(sesion);
        break;
      }
      case "checkout.session.async_payment_failed":
        await avisarPagoFallido(evento.data.object); // el pago diferido no se completó
        break;
    }
  } catch {
    return new Response("Error", { status: 500 }); // Stripe reintentará el evento
  }

  await marcarEventoProcesado(evento.id);
  return new Response("ok", { status: 200 });
}

async function cumplirPedido(sesion) {
  // Vincula la sesión con tu pedido mediante client_reference_id, que tú enviaste al crearla
  const pedido = await buscarPedido(sesion.client_reference_id);
  if (!pedido || pedido.estado === "entregado") return; // desconocido o ya entregado
  if (sesion.amount_total !== pedido.montoEnCentavos || sesion.currency !== pedido.moneda) {
    throw new Error("El pago no coincide con el pedido");
  }
  await activarCompra(pedido);
}

Hay tres detalles que este ejemplo cuida y que la documentación de Stripe destaca:

  • Confirma el estado del pago. checkout.session.completed se dispara cuando termina el Checkout, no siempre cuando el dinero está confirmado. Entrega solo si payment_status es paid.
  • Atiende los pagos diferidos. En métodos como las transferencias bancarias, la sesión se completa con el pago aún pendiente. Stripe envía checkout.session.async_payment_succeeded cuando se confirma y checkout.session.async_payment_failed si falla; este último es el momento de avisar al cliente. Suscribe tu endpoint a estos tres eventos.
  • Comprueba el pedido. Envía tu identificador de pedido en client_reference_id al crear la sesión y, al recibir el evento, verifica que el pedido existe, que el monto y la moneda coinciden y que no se entregó antes.

Las funciones eventoYaProcesado, marcarEventoProcesado, buscarPedido, activarCompra y avisarPagoFallido son tuyas: una tabla con el identificador del evento como clave única resuelve la idempotencia, y el estado del pedido evita entregar dos veces si un mismo pago genera más de un evento. Stripe recomienda además escuchar solo los tipos de evento que necesitas y renovar el secreto periódicamente o si sospechas que se filtró; al renovarlo puedes dejar el secreto anterior activo hasta 24 horas para actualizar tu código sin perder eventos.

MercadoPago

MercadoPago envía el encabezado x-signature, con un formato como ts=...,v1=..., junto con x-request-id; el identificador del pago llega en la consulta como data.id. La clave secreta se genera al guardar la configuración de webhooks en "Tus integraciones". Sus SDK oficiales traen un validador de firma. Con el SDK de JavaScript, la verificación en una ruta de Next.js se ve así:

// app/api/webhooks/mercadopago/route.js
import { WebhookSignatureValidator, InvalidWebhookSignatureError } from "mercadopago";

export async function POST(req) {
  const url = new URL(req.url);
  try {
    WebhookSignatureValidator.validate({
      xSignature: req.headers.get("x-signature"),
      xRequestId: req.headers.get("x-request-id"),
      dataId: url.searchParams.get("data.id"),
      secret: process.env.MP_WEBHOOK_SECRET,
    });
  } catch (err) {
    if (err instanceof InvalidWebhookSignatureError) {
      return new Response("Firma inválida", { status: 401 });
    }
    throw err;
  }

  // Firma válida: ahora consulta el pago real en la API de MercadoPago
  // GET https://api.mercadopago.com/v1/payments/{id}
  // y compara estado, monto y tu referencia externa con lo que esperabas.
  return new Response("ok", { status: 200 });
}

Dos detalles importantes de la documentación de MercadoPago:

  • La notificación trae el identificador del recurso, pero no los datos completos del pago. Después de validar la firma, consulta el pago en https://api.mercadopago.com/v1/payments/[ID] para conocer su estado real, y comprueba que el monto y la referencia coinciden con tu pedido antes de activar nada.
  • Debes responder con HTTP 200 o 201. El tiempo de espera es de 22 segundos; si no recibe la confirmación, MercadoPago reintenta cada 15 minutos al principio y después de tres intentos amplía el intervalo, sin dejar de insistir. Otra razón para que tu endpoint sea idempotente.

MercadoPago recomienda usar dos URL distintas, una para pruebas con credenciales de prueba y otra para producción. Respeta esa separación y no mezcles credenciales.

Cómo comprobarlo: envía una solicitud con una firma inventada (con curl) y confirma que recibes 400 o 401 y que no se activa nada. Luego repite un evento real dos veces y verifica que el producto se entrega una sola vez.

5. Copias de seguridad y una restauración de prueba

Una copia que nunca restauraste es una esperanza, no un respaldo. Si usas una base de datos administrada, revisa qué ofrece tu proveedor y haz una restauración a una base nueva. Con PostgreSQL propio:

pg_dump --format=custom "$DATABASE_URL" -f respaldo.dump
createdb prueba_restauracion
pg_restore --no-owner -d prueba_restauracion respaldo.dump
psql prueba_restauracion -c "select count(*) from usuarios;"

Compara el conteo con el de producción. Anota cuánto tardó: ese es tu tiempo de recuperación real. Guarda las copias fuera del servidor que respaldan y no las dejes en la misma carpeta del proyecto.

6. Registros (logs) sin datos personales

Los asistentes de IA suelen añadir console.log(req.body) para depurar y se queda para siempre. Ese registro termina con correos, teléfonos, tokens y, en el peor caso, datos de pago.

  • Registra identificadores (pedido 981 pagado), no contenidos.
  • Nunca registres encabezados Authorization, cookies, claves ni cuerpos completos de webhooks.
  • Define cuánto tiempo guardas los registros y quién puede leerlos.

Cómo comprobarlo: haz un registro de usuario y una compra de prueba, y busca en los registros tu correo y los últimos dígitos de la tarjeta de prueba. No deberían aparecer. Si trabajas con datos de clientes, revisa también la checklist de privacidad para agentes de IA en pymes.

7. Una prueba de punta a punta que "compre de verdad"

Con cincuenta pruebas unitarias en verde, tu app puede no cobrar. Una prueba de punta a punta recorre el camino completo: registro, elegir producto, pagar en modo de prueba, recibir el webhook y comprobar que el acceso quedó activo. Es la prueba que más se parece a lo que hará tu cliente.

Para Stripe, el modo de prueba usa tarjetas de prueba como 4242 4242 4242 4242 y la CLI de Stripe reenvía los eventos a tu computadora:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

La CLI imprime el secreto whsec_ de esa sesión, distinto del de tu endpoint en el panel: no los mezcles. Para MercadoPago, usa las credenciales de prueba y la URL de prueba, y simula notificaciones desde "Tus integraciones". Con Playwright, el esqueleto de la prueba es:

import { test, expect } from "@playwright/test";

test("compra completa en modo de prueba", async ({ page }) => {
  await page.goto("/registro");
  // ...crea un usuario de prueba, elige el producto y completa el pago de prueba...
  await expect(page.getByText("Compra confirmada")).toBeVisible();
  // ...y verifica en la base de datos que el acceso quedó activo una sola vez
});

Cubre también los casos que duelen: pago rechazado, webhook duplicado y firma inválida. Pídele a tu asistente que escriba la prueba, pero ejecútala tú y mira que realmente pase.

Una regla útil para tu archivo AGENTS.md o equivalente:

- Antes de dar una tarea por terminada, ejecuta la prueba de punta a punta y muestra el resultado.
- Nunca escribas claves ni secretos en el código ni en archivos que se suban a git.
- Pregunta antes de cambiar migraciones de base de datos o la lógica de pagos.

8. Revisión final: haz que la IA interrogue lo que construyó

Cuando termines, pide una revisión con un rol distinto al de quien escribió el código. Dos mensajes que puedes copiar:

Actúa como revisor de seguridad de esta aplicación antes de salir a producción.
Recorre todas las rutas de la API y haz una tabla con: ruta, método, si exige
autenticación, si comprueba que el recurso pertenece al usuario y si valida la
entrada. Marca en rojo cualquier ruta que falle y explica cómo la explotaría
alguien. No arregles nada todavía.
Busca en el repositorio y en su historial cualquier clave, token o secreto.
Revisa qué variables de entorno llegan al navegador. Revisa el manejo de los
webhooks de pago: verificación de firma, idempotencia y confirmación del monto.
Dame una lista priorizada por riesgo, con el archivo y la línea de cada hallazgo.

La IA puede encontrar problemas que pasaste por alto, pero también puede dar por bueno algo que no lo es. Trata su informe como una lista de cosas por comprobar, no como una garantía: reproduce cada hallazgo antes de corregirlo y vuelve a ejecutar las pruebas después.

Checklist para imprimir

  • Ninguna clave secreta en el frontend, en variables públicas ni en el historial de git
  • Claves rotadas si alguna llegó a un repositorio
  • Tope de gasto en el proveedor de IA y límite por usuario en tu endpoint
  • Autenticación en cada ruta y comprobación de propiedad de cada recurso
  • Firma de webhook verificada, pago confirmado en la API y procesamiento idempotente
  • Copia de seguridad restaurada al menos una vez en una base de prueba
  • Registros sin correos, tokens ni datos de pago
  • Prueba de punta a punta en modo de prueba con pago aprobado, rechazado y evento duplicado

Preguntas frecuentes

¿Necesito saber programar para aplicar esta lista?

Para entender los fragmentos, ayuda saber leer código. Si no programas, usa la lista como guion de revisión con quien te ayuda a desarrollar y pídele evidencia de cada punto: una captura de la prueba, el resultado del comando, la tabla de rutas.

¿Es suficiente con pedirle a la IA que haga la app "segura"?

No. Un pedido genérico produce cambios genéricos. Pide controles concretos, como los de esta lista, y verifica cada uno con una prueba que puedas ejecutar tú.

¿Qué hago si ya lancé y no revisé nada de esto?

Empieza por lo que cuesta dinero: rota las claves expuestas, pon un tope de gasto en el proveedor y revisa que los webhooks verifiquen la firma. Después sigue con autenticación, copias de seguridad y registros.

¿Cuánto tarda hacer toda la revisión?

Depende del tamaño de tu app. Para un proyecto pequeño, cada punto se puede revisar en menos de una hora; la prueba de punta a punta y la restauración de la copia son las que más tiempo llevan la primera vez.

¿Esta lista reemplaza una auditoría de seguridad?

No. Cubre los fallos más comunes de una app recién lanzada. Si manejas datos sensibles, como salud o finanzas de terceros, o muchos pagos, una revisión profesional sigue siendo recomendable.

Si estás empezando con asistentes de código, qué es el vibe coding y cómo usar Codex y Claude para crear proyectos pequeños te dan el contexto para aplicar esta lista. Para aprender a trabajar con Claude desde cero, tienes el curso Claude para Principiantes.

ChatGPT para Principiantes

Consulta las lecciones, los ejercicios disponibles y las condiciones de acceso en la página de este curso.

Ver temario y opciones de acceso

Preguntas frecuentes

Preguntas que este tema suele generar

¿Necesito saber programar para aplicar esta lista?
Ayuda saber leer código, pero puedes usar la lista como guion de revisión y pedir evidencia de cada punto a quien desarrolla tu app: captura de la prueba, resultado del comando o tabla de rutas.
¿Es suficiente con pedirle a la IA que haga la app segura?
No. Un pedido genérico produce cambios genéricos. Pide controles concretos y verifica cada uno con una prueba que puedas ejecutar tú.
¿Qué hago si ya lancé y no revisé nada de esto?
Empieza por lo que cuesta dinero: rota las claves expuestas, pon un tope de gasto en el proveedor y comprueba que los webhooks verifiquen la firma. Después sigue con autenticación, copias de seguridad y registros.
¿Esta lista reemplaza una auditoría de seguridad?
No. Cubre los fallos más comunes de una app recién lanzada. Si manejas datos sensibles o muchos pagos, una revisión profesional sigue siendo recomendable.

Fuentes

Referencias externas

  1. Best Practices for API Key Safety— OpenAI
  2. Manage webhook endpoints (firmas y buenas prácticas)— Stripe
  3. Webhooks: validar el origen de una notificación— Mercado Pago Developers
  4. Secret scanning— GitHub

Siguiente curso recomendado

ChatGPT para el Trabajo

Curso intensivo de ocho clases para transformar pedidos, archivos y datos en comunicaciones, contenido, investigación, análisis y planes profesionales verificables. 8 clases. Este curso por US$20, en un solo pago. También está incluido en el acceso a toda la escuela.

La matrícula incluye todos los cursos actuales y los nuevos que publiquemos, libros, prompts y materiales. Paga en 3, 6 o 12 cuotas (desde US$9 al mes) o US$80 en un pago.

Ayuda y soporte