Prompts para Auditar Seguridad de Código con IA
10 prompts para revisar seguridad con IA: OWASP Top 10, SQL injection, XSS, secretos, autenticación y dependencias vulnerables. Con límites claros de la IA.
Tabla de contenidos
Un día te llega un aviso de tu proveedor de email: hay una cuenta con tu dominio enviando phishing a dos mil clientes. Tú no has escrito ese código. Lo escribió tu editor con IA hace dos meses, en un fichero que ni recordabas. Nadie revisó esa PR porque era “solo un endpoint de recuperación de contraseña”. Ahí empieza el peor camino posible: un incidente que no entiendes, en código que no conoces, generado por una máquina que no responde por nada.
La realidad es más incómoda. Los modelos de lenguaje son excelentes reconociendo patrones de vulnerabilidad conocidos. Saben cómo es un SQL injection mal construido porque lo han visto miles de veces en auditorías públicas. Saben que eval() es un problema. Saben identificar un token con el formato ghp_. Lo que no pueden es saber si ese fragmento es alcanzable desde Internet, si la validación que hay tres archivos más arriba lo neutraliza, o si el impacto real de un path traversal en ese endpoint concreto es un 404 molesto o una lectura arbitraria de tu sistema de ficheros.
Por eso este artículo tiene dos mitades. La primera son diez prompts que producen revisiones útiles y accionables. La segunda, igual de importante, es una lista concreta de lo que ninguna IA te va a encontrar por mucho que le pidas que lo haga. Está al final, es la parte más importante del texto y no la he suavizado.
Si quieres el contexto general de cómo trabajar con IA sin que te pise, empieza por programar con IA sin arruinarte y por cómo testear el código que genera la IA.
Seis reglas antes de auditar nada con un modelo
1. Nunca des por bueno un hallazgo sin ejecutarlo. Todo lo que te devuelva tiene que acabar en un curl, un test o una lectura del código que te dé la razón. Si no puedes reproducirlo, no es un hallazgo, es una hipótesis.
2. Pide siempre ruta, línea y PoC. Sin ubicación exacta no hay acción posible. Un prompt que no exige archivo:línea te devuelve párrafos vagos que parecen convincentes y no se pueden corregir.
3. Dale el framework, porque el framework cambia el veredicto. El mismo código puede ser XSS o ser la forma correcta según si React escapa o si estás en un motor de plantillas. La IA no lo sabe si no se lo dices.
4. Separa hallazgos confirmados de hipótesis. Oblígalo a etiquetar cada punto como CONFIRMADO, PROBABLE o SOSPECHOSO. La diferencia entre las tres categorías es la diferencia entre arreglar un bug hoy y perder una tarde persiguiendo un falso positivo.
5. Dale contexto de exposición. Una ruta protegida por admin y un endpoint público sin auth tienen el mismo código y una criticidad que se diferencia en diez niveles. Diles cuáles rutas son públicas.
6. Aplica OWASP explícitamente. No asumas que la IA conoce la taxonomía actual. Pásale la lista de categorías que quieres que compruebe y dale espacio para responder categoría por categoría.
1. Auditoría OWASP Top 10 contra código real
Actúa como un analista de seguridad de aplicaciones. Voy a darte código
real para que lo audites.
Stack: [Node.js + Express | Next.js | NestJS | Django | ...]
Versiones: [las que importan, ej: Next 15, Express 5]
Contexto de las rutas: [cuáles son públicas y cuáles requieren auth,
qué roles tienen, si hay multitenancy]
CÓDIGO:
[pegar el contenido de los ficheros relevantes, o la lista de ficheros
si son muchos, y que te los pida uno a uno]
Audita contra estas categorías de OWASP Top 10 2021, una por una y en
este orden. Para CADA categoría dame:
1. ¿Hay hallazgos en mi código? Sí / No / No determinable con el
fragmento disponible.
2. Cada hallazgo con: categoría OWASP, archivo y línea, el fragmento
exacto, por qué es vulnerable, un escenario de explotación concreto
(con petición HTTP de ejemplo si es posible), el impacto real en
MI aplicación, y el arreglo con el diff.
3. Etiqueta cada hallazgo como CONFIRMADO / PROBABLE / SOSPECHOSO con
la razón de esa etiqueta.
Las categorías:
A01 Broken Access Control
A02 Cryptographic Failures
A03 Injection
A04 Insecure Design
A05 Security Misconfiguration
A06 Vulnerable and Outdated Components
A07 Identification and Authentication Failures
A08 Software and Data Integrity Failures
A09 Security Logging and Monitoring Failures
A10 Server-Side Request Forgery
Si el fragmento que te doy no alcanza para decidir, dilo y pídeme el
fichero que falta en lugar de suponer. Prefiero un "no determinable"
honesto a un hallazgo inventado.
Por qué funciona: recorrer la taxonomía categoría por categoría convierte una revisión difusa en una lista de comprobación real. Y la instrucción “no determinable, pídeme el fichero” es la que evita los falsos positivos más caros: el modelo rellenando huecos con supuestos razonables pero falsos.
2. Detección de SQL injection
Revisa específicamente inyección SQL en este código de acceso a datos.
[pegar el módulo de acceso a datos completo]
Stack de acceso: [Prisma | TypeORM | Knex | SQL crudo | Mongoose]
Base de datos: [PostgreSQL | MySQL | MongoDB]
Busca estos patrones, uno por uno, y reporta cada aparición:
1. Concatenación o template literals en una query:
`WHERE id = ${id}` o "WHERE id = " + id → inyección directa
2. String.format, sprintf o replace con valor de usuario en una query
3. query() o $queryRawUnsafe() de Prisma con interpolación
(distinguir de $queryRaw con tagged template, que sí es seguro)
4. findMany con where construido a mano en lugar de objeto tipado
5. Object.keys(req.body) usados para construir el where sin whitelist
6. raw() de Sequelize o knex.raw() con variables
7. Nombres de columna o tabla provenientes del usuario
8. ORDER BY con el valor vindo de un query param sin validar
(es el más olvidado: no lleva comillas, así que escapar los
paréntesis ya lo convierte en un exploit)
9. LIKE con % y _ sin escapar, para contenido dinámico
10. MongoDB con objetos de usuario en el filtro sin sanear
(operador $where, o claves que empiezan por $)
Para cada hallazgo dame el archivo:línea, el fragmento, una petición
concreta que lo explotaría (ej: ?id=1' OR '1'='1) y el código corregido.
Si una consulta ya usa parámetros, dilo y clasifícalo como SEGURO
explícitamente. Quiero también la lista de consultas seguras, para
saber que he revisado el fichero entero y no solo lo sospechoso.
Por qué funciona: enumerar los diez patrones cubre la práctica totalidad de inyecciones reales, y el punto 8 (el ORDER BY) es un agujero que casi nadie cierra porque no lleva comillas y por eso todos los sanitizadores automáticos lo dejan pasar. Pedir la lista de consultas seguras además de las vulnerables garantiza cobertura real del fichero.
Ejemplo real de lo que devuelve, y su arreglo:
// ❌ ORDER BY con valor de usuario sin whitelist
app.get('/events', async (req, res) => {
const orderBy = req.query.sort as string;
const events = await prisma.$queryRawUnsafe(
`SELECT * FROM events ORDER BY ${orderBy}`
);
});
// ✅ Whitelist explícita de columnas ordenables
const SORTABLE = ['createdAt', 'title', 'eventCount'] as const;
type Sortable = (typeof SORTABLE)[number];
const sort: Sortable = SORTABLE.includes(req.query.sort as Sortable)
? (req.query.sort as Sortable)
: 'createdAt';
const events = await prisma.event.findMany({
orderBy: { [sort]: req.query.dir === 'asc' ? 'asc' : 'desc' }
});
3. XSS y lo que escapa y lo que no
Revisa mi código frontend y backend en busca de XSS.
Frontend: [React | Vue | Svelte | Angular | HTML con Jinja | Handlebars]
[pegar los componentes o plantillas relevantes]
Backends: [los que devuelven HTML o construyen emails]
[pegar los fragmentos que generan HTML]
Analiza punto por punto:
1. dangerouslySetInnerHTML con datos que vienen de la API o del usuario
2. dangerouslySetInnerHTML en componentes que reciben HTML de un CMS
3. insertAdjacentHTML, outerHTML, document.write
4. eval, new Function, setTimeout con string
5. window.open o location.href con valor de query param
(javascript: URLs y open redirect)
6. href={valorDeUsuario} sin validación de protocolo
7. target="_blank" sin rel="noopener noreferrer" (reverse tabnabbing)
8. sanitización con una regex propia en vez de DOMPurify o
isomorphic-dompurify
9. Emails en HTML generados con contenido del usuario (email injection)
10. SVG inline con atributos onload o referencias a scripts externos
Para cada punto, clasifícalo:
- SEGURO: el valor es una constante o viene de un origen controlado
- VULNERABLE: hay un camino desde entrada de usuario hasta el sink
- NO APLICABLE: el sink no existe en mi stack
En los VULNERABLES necesito: archivo:línea, el camino completo desde
el input (¿de dónde viene el valor?) hasta el sink, un payload XSS
concreto que lo exploitearía, y la corrección con la librería
apropiada.
Criterio importante: React escapa por defecto, así que {valor} es
seguro en texto y en atributos, pero NO en URLs (javascript:), ni
dentro de dangerouslySetInnerHTML, ni en href/src con protocolos no
validados.
Por qué funciona: la distinción entre “React escapa por defecto” y “React no escapa un href” es donde se cuelan los XSS reales en aplicaciones que parecen limpias. Pedir la clasificación SEGURO/VULNERABLE/NO APLICABLE sobre una lista explícita de sinks evita que el modelo se limite a mencionar dangerouslySetInnerHTML y pase por alto el open redirect.
4. Secretos hardcodeados y fugas por logging
Busca secretos y datos sensibles hardcodeados en este repositorio, y
puntos donde se filtran datos por logs, errores o respuestas.
Código a revisar: [pegar, o indicar que es un repo con estos ficheros]
Dos búsquedas separadas:
A) SECRETOS EN EL CÓDIGO
- Claves de API con formato reconocible: sk-, ghp_, AKIA, xoxb-,
gsk_, AIza, npm_, glpat-, hf_
- Cadenas de conexión con usuario y contraseña embebidas
- Tokens de JWT con firma embebida
- Passwords, secrets o apiKey con un string literal
- Claves privadas (-----BEGIN ... PRIVATE KEY-----)
- URLs de webhooks de Slack o Discord (filtran el proyecto entero)
- .env.example con valores reales en lugar de placeholders
B) FUGAS EN TIEMPO DE EJECUCIÓN
- console.log o logger.info con req.body, req.headers o req.user
- Mensajes de error que devuelven el stack trace al cliente
- UnhandledRejection o middleware de error que imprime el error
completo a la respuesta
- Logs que incluyen el Authorization header o las cookies
- Sentry o similar con sendDefaultPii activado
- Analytics que envían URLs con parámetros sensibles
- Responses que devuelven el usuario completo, incluido un campo
passwordHash que no debería salir
Para cada hallazgo: archivo:línea, el valor ENMASCARADO (primeros 4
y últimos 4 caracteres, NUNCA el valor completo), por qué es un
problema, el impacto si se filtra, y el arreglo.
Si un secreto está hardcodeado, además del arreglo, dime qué hay que
hacer EN EL SERVIDOR: rotar la credencial, revisar los logs del
proveedor y comprobar si está en el historial de git.
Ese último punto es el que se olvida siempre.
Por qué funciona: separar secretos en el código de fugas en runtime cubre las dos dimensiones del problema, y la parte de “qué hacer en el servidor” convierte un hallazgo de código en un incidente de seguridad gestionado. La instrucción de enmascarar evita que al copiar el informe a un ticket expongas la credencial otra vez.
5. Broken access control: el que más se cuela y nadie ve
Revisa el control de acceso de mi API. Este es el OWASP número uno
y suele ser el peor cubierto.
Arquitectura: [cómo separo tenants y usuarios, ej: userId en el JWT,
middleware global, servicio de permisos]
Rutas: [pegar los controllers y el middleware de auth]
Para CADA endpoint protegido, responde a estas cinco preguntas:
1. AUTORIZACIÓN: ¿se comprueba que el usuario es dueño del recurso
que pide, o solo que tiene un token válido? Busca los casos donde
un usuario autenticado podría pedir el recurso de otro cambiando un
ID en la URL (IDOR).
2. AISLAMIENTO POR TENANT: en las consultas a base de datos, ¿el
filtro de tenant está siempre? Busca los findUnique({ where: { id } })
que no filtran por tenant. Estos son IDOR en potencia.
3. MASIFICACIÓN DE DATOS: ¿un endpoint de listado filtra por el
usuario o devuelve todo? Comprueba los findMany() sin where.
4. ROLES: ¿el check de rol es "si es admin" o es un sistema de
permisos granular? ¿Hay bypass posible con arrays vacíos o
comparaciones laxas?
5. MASS ASSIGNMENT: ¿se usa Object.assign o el spread del body
directamente sobre el modelo? Eso permite escribir en campos que
no deberían ser escribibles (role, userId, isAdmin).
Para cada endpoint dame una tabla: ruta, expone recurso ajeno (Sí/No),
filtra por tenant (Sí/No), comprueba rol (Sí/No), vulnerable a mass
assignment (Sí/No).
Y dame los tres hallazgos más graves con su PoC: una petición curl
con el token de un usuario legítimo pidiendo datos de otro.
Por qué funciona: la lista de cinco preguntas convierte “revisa el access control” en algo accionable, y el patrón exacto de findUnique({ where: { id } }) sin filtro de tenant produce el grueso de las brechas de datos en SaaS. Pedir las PoC con dos usuarios distintos convierte el hallazgo en algo que cualquiera puede verificar en dos minutos.
6. Validación de entrada, allowlists y denegación de servicio
Revisa la validación de entrada de mi API. Busca el equilibrio
invertido: demasiadas validaciones superficiales en unos sitios y
cero en los que importan.
Stack: [Zod | Joi | class-validator | Yup | validación manual]
[pegar los schemas de validación y los controllers]
Analiza:
1. Endpoints SIN schema de validación. Lista todos los que reciben
body, query o params y no validan contra un schema declarado.
2. Validación solo de tipo pero no de rango ni de formato:
z.string() acepta "", " ", "<script>" y un string de 10 MB.
¿Qué longitud máxima, qué formato y qué whitelist hay?
3. z.coerce.number() o parseInt: ¿se comprueba NaN después?
Es el bug clásico: parseInt("abc") es NaN y no lanza error.
4. Enums validados con z.enum frente a comparaciones libres.
5. Validación en el middleware frente a validación solo en el
controller, y si hay rutas que se saltan el middleware.
6. Body size limits: ¿hay límite en express.json()? Sin él, un body
de 100 MB es una denegación de servicio gratis.
7. Validación del Content-Type antes de parsear.
8. URLs que vienen del usuario: ¿se valida el protocolo y se
bloquean los rangos de IP internos? (ver prompt 7)
9. Rutas de ficheros construidas con input del usuario (path
traversal): ¿se valida que la ruta resuelta esté dentro del
directorio permitido? Validar el input con una regex no basta:
la comprobación correcta es sobre la ruta ya resuelta.
10. Regex con backtracking exponencial en la validación: un patrón
tipo (a+)+ sobre input largo es una denegación de servicio
trivial de disparar.
Para cada hueco, el código de validación corregido usando MI librería
y MI estilo actual, para que sea un drop-in replacement.
Distingue los casos donde validar NO es la respuesta correcta: por
ejemplo, un campo que solo puede ser "sí o no" y cuyo valor inválido
debe ignorarse en vez de devolver 422.
Por qué funciona: z.coerce.number() con NaN y express.json() sin límite son los dos valores por defecto que aparecen en casi todas las apps y que nadie revisa porque no se ven en la UI. Y el punto 9 (validar la ruta resuelta, no el input) corrige la defensa más común y menos eficaz contra path traversal.
7. SSRF y llamadas a URLs externas
Revisa mi código en busca de SSRF.
Dónde hago peticiones salientes a URLs controladas por el usuario:
[pegar los fetch/axios/SDK que reciben URLs o hosts externos]
Contexto: [qué procesos hacen esto: webhook de usuario, importador,
preview de enlaces, descarga de imágenes]
Para cada salida, dime:
1. ¿La URL o el host viene de entrada de usuario, total o parcialmente?
2. ¿Qué protocolo se acepta? Debe ser https (y http solo si es
imprescindible). ¿Se filtra javascript:, file:, gopher:, data:?
3. ¿Se comprueban los rangos de IP internos? Bloquear:
127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16
(link-local y metadatos de cloud), ::1, fc00::/7
4. ¿Hay riesgo de redirect? Un 302 hacia 169.254.169.254 salta
cualquier validación hecha sobre la URL inicial. Hay que
desactiva los redirects o comprobar cada uno.
5. ¿Se resuelve el DNS antes de conectar? Un hostname del atacante
puede resolver a una IP interna.
6. ¿Hay riesgo de DNS rebinding, donde el tiempo de resolución
distinto entre validación y conexión cambia el destino?
7. ¿La respuesta se devuelve al usuario? Si es sí, puede filtrar
metadatos de cloud, que en muchos entornos incluyen credenciales.
Para cada salida vulnerable, el patrón de mitigación completo:
validación de protocolo, resolución de IP y comprobación de rangos,
allowlist de hosts si aplica, timeout, límite de tamaño de respuesta,
y desactivación de redirects.
En mi caso puedo permitir solo una lista cerrada de hosts
([listar hosts]). Dame cómo mantener esa lista sincronizada con la
configuración, sin editar código cada vez que añado un proveedor.
Por qué funciona: el punto 4 (los redirects) es lo que hace inútil la mayoría de validaciones de SSRF hechas en casa: validar la URL inicial y dejar que la librería siga redirecciones permite el bypass en dos líneas. Mencionarlo convierte una recomendación ingenua en una que realmente funciona.
8. Autenticación, sesiones y JWT
Revisa mi implementación de autenticación.
[pegar el middleware de auth, la lógica de login, la emisión y
validación de tokens, y la gestión de sesión y cookies]
Contexto de despliegue: [número de réplicas, si es stateless,
dónde se guarda el estado]
Checklist con respuesta concreta para cada punto:
1. ALGORITMO JWT: ¿se valida explícitamente el algoritmo en verify()?
El ataque del "alg: none" y el HS/RS confusion siguen vivos si
pasas el secret a verify sin restricciones. ¿Usas una lista
blanca de algoritmos?
2. EXPIRACIÓN: ¿el access token tiene expiración corta? ¿El refresh
token tiene rotación con revocación del anterior?
3. ALMACENAMIENTO EN EL CLIENTE: ¿el token está en localStorage?
¿Sabes qué implica eso ante un XSS? ¿Considero cookie httpOnly
con SameSite, o esa decisión está tomada y documentada?
4. COOKIES: ¿Secure, HttpOnly y SameSite=Lax o Strict en todas las
cookies de sesión? ¿Hay algún maxAge que la convierta en
permanente?
5. SECRETOS: ¿el JWT secret viene de variable de entorno con rotación
o está en el repositorio? ¿La firma usa HS256 con un secret corto?
6. RATE LIMITING en login y en recuperación de contraseña. Sin él,
hay fuerza bruta de credenciales sin límite.
7. RESET DE CONTRASEÑA: ¿el token es de un solo uso? ¿caduca?
¿se invalida al usarse? ¿el enlace evita filtrar el token por el
referrer?
8. ENUMERACIÓN DE USUARIOS: ¿login y registro devuelven mensajes
distintos según el email exista? Eso es una fuga de información.
9. SESIÓN FIJA: ¿se regenera el identificador de sesión tras el login?
10. LOGOUT: ¿el token se revoca de verdad en el servidor, o solo se
borra del cliente?
Para cada fallo, la corrección y el impacto. Marca los tres más graves.
Por qué funciona: los puntos 1 y 5 son los que un revisor humano se salta porque “ya lo hice bien” y son los que más se rompen en la práctica. La lista blanca de algoritmos en verify() es un detalle de una línea que evita una vulnerabilidad completa.
9. Dependencias: lo que un scanner ya hace mejor que la IA
Audita las dependencias de mi proyecto.
package.json: [pegar]
package-lock.json: [no hace falta que lo pegues entero]
Ejecuta mentalmente el análisis, pero quiero el razonamiento, no
solo la lista:
1. Dependencias con vulnerabilidad conocida (CVE) en el árbol de
producción. Para cada una: paquete, versión instalada, versión
corregida, severidad, y si es ALCANZABLE desde mi código.
Alcanzable significa que hay un import que llega a la función
vulnerable. Si no es alcanzable, la severidad real baja y eso
cambia la prioridad.
2. Dependencias de desarrollo con CVE: si solo se usan en CI o en
tests, el riesgo es distinto. Sepáralo.
3. Dependencias abandonadas: última publicación, issues abiertas, y
si el mantenedor recomienda sustituirla por otra.
4. Dependencias duplicadas por rangos incompatibles que acaban
instalando dos copias de la misma librería, con el bloat y el
instanceof roto entre módulos que eso provoca.
5. Paquetes que podrían ser typosquatting, con nombres muy parecidos
a populares.
6. Postinstall scripts: dependencias con script de instalación que
ejecutan código. Son un vector de ejecución arbitraria en tu máquina.
7. Licencias incompatibles con el uso que hago de las dependencias.
Para cada hallazgo crítico dame: paquete, versión, comando para
corregir (npm audit fix --force o actualización manual), y si
actualizar a la versión corregida rompe algo (breaking changes).
Y con franqueza: en qué casos npm audit y las herramientas
dedicadas ya hacen mejor este trabajo que tú. Quiero que uses la IA
para lo que las herramientas no cubren, no para repetir su output.
Por qué funciona: el punto 1 con el criterio de “alcanzable desde mi código” es lo que separa un informe accionable de los 400 avisos que te da npm audit y que nadie lee. Y la última frase es intencional: parte de este prompt sirve para que la propia IA te diga cuándo usar la herramienta en vez de meterse en un terreno donde pierde.
10. El prompt de honestidad: qué no puede detectar la IA
Antes de darme tu informe final, quiero que seas honesto conmigo sobre
los límites de este análisis.
Responde a estas preguntas con franqueza, sin suavizar:
1. ¿Qué porcentaje de las rutas de mi aplicación crees que has
podido revisar realmente? Si solo te he dado 3 ficheros de 40,
dilo con el número exacto.
2. ¿Qué vulnerabilidades son IMPOSIBLES de detectar leyendo código?
Razona sobre MI aplicación concreta, no en abstracto.
3. ¿Qué vulnerabilidades requieren ejecución para confirmarse?
Enuméralas específicamente para mi caso.
4. ¿Qué has dado por supuesto sin poder verificarlo? Lista cada
supuesto: "he asumido que X", "he asumido que Y".
5. ¿Hay algún hallazgo que sea probable que sea FALSO positivo? Sé
honesto con los dudosos, no solo con los seguros.
6. ¿Qué necesito hacer YO, como humano, para completar esta
auditoría? Lista de acciones concretas y priorizadas, con la
herramienta sugerida para cada una.
7. Redacta el resumen ejecutivo de 5 líneas que leería un CTO con
30 segundos: qué hay que arreglar esta semana, qué hay que mirar
este mes, y qué necesita un pentest profesional.
Regla: prefiero un informe que cubra el 60% de la superficie con un
95% de precisión que uno del 100% con un 40%. Si dudas entre
reportar algo y callártelo, reportarlo con la etiqueta SOSPECHOSO.
Por qué funciona: este es el prompt más importante del artículo. Obligar al modelo a cuantificar su cobertura y a declarar sus supuestos convierte un informe con formato de auditoría profesional en algo que sabes cómo interpretar. Y la regla final evita que el modelo se quede callado ante lo que no está seguro, que es donde aparecen los hallazgos más interesante.
Lo que ninguna IA va a encontrarte
Esta sección no es un prompt. Es la parte que hace que el resto sea utilizable.
1. Vulnerabilidades de lógica de negocio. Tu código permite transferir un pedido a una cuenta que no es tuya porque nadie pensó en comprobar la pertenencia, y la regla está repartida en tres handlers distintos. Ningún modelo ve eso, porque no está en un fichero: está en el comportamiento del sistema.
2. Problemas de permisos que solo aparecen al encadenar peticiones. Un endpoint devuelve datos que no debería, pero solo cuando lo llamas después de otro que te da un identificador válido. Eso requiere ejecución, no lectura.
3. Condiciones de carrera. El doble gasto de un saldo, el coupon aplicado dos veces, la compra del último producto por dos usuarios a la vez. Ninguna IA ve carreras: no puede ejecutar dos peticiones a la vez.
4. Configuración del entorno de producción. El bucket de S3 en público, el CORS con * y credenciales, el panel de administración expuesto, un backup que baja al bucket público. Nada de eso está en el repositorio.
5. Configuración incorrecta de dependencias e infraestructura. Un trust proxy mal puesto, un header de seguridad ausente, un servicio de base de datos accesible desde Internet sin VPN, un role de IAM con permisos de administrador. El código es perfecto y el sistema es una puerta abierta.
6. Lo que depende de la lógica de tus terceros. Si tu proveedor de auth tiene un fallo o una clave está mal revocada, ningún análisis de tu código lo encuentra.
7. Lo que no sabes que tienes. Endpoints olvidados en un servicio que se deployó hace un año, un panel de admin de un CMS, un backup con la base de datos en el directorio público. Ninguna auditoría de código encuentra lo que nadie sabe que existe.
8. Vulnerabilidades físicas y de proceso. Impresión de datos sensibles, accesos a la oficina, ingeniería social. Fuera del alcance de un modelo que solo lee texto.
| Lo que sí detecta bien | Lo que no detecta nunca |
|---|---|
| Patrones de inyección conocidos | Lógica de negocio rota |
| Secretos con formato reconocible | Condiciones de carrera |
| XSS por sinks conocidos | Configuración de infraestructura |
| Ausencia de validación en rutas | Permisos que dependen del flujo |
| Comparaciones de tokens sin timing-safe | Qué endpoints existen y no documentas |
| Patrones de IDOR en el código | Fallos en dependencias de terceros |
| Versiones con CVE conocidas | Accesibilidad física y social |
Anti-patrones: cómo se malgasta una auditoría con IA
❌ Aceptar el informe sin ejecutarlo. El modelo te dice que hay un SQL injection en lib/db.ts:42. Puede ser cierto, puede ser una string interpolada que Prisma sanitiza. Si no lo compruebas, o arreglas algo que no estaba roto, o dejas de arreglando algo que sí lo estaba.
✅ Cada hallazgo se convierte en un test que falla antes del arreglo y pasa después. Eso no solo lo verifica, lo documenta.
// ❌ Confiar en la palabra del modelo
it('TODO: revisar SQL injection', () => {});
// ✅ El test es la prueba de concepto del hallazgo
it('rechaza un id con inyección SQL en el filtro de búsqueda', async () => {
const res = await request(app)
.get('/events')
.query({ q: "'; DROP TABLE events; --" });
expect(res.status).toBe(400);
expect(await prisma.event.count()).toBeGreaterThan(0);
});
❌ Revisar solo el código nuevo. Si el modelo se pasa el repositorio entero cada vez, es lento y caro. Si solo le pasas el diff, pierdes los hallazgos en código antiguo que sigue en producción.
✅ Dos pasadas: una revisión completa ocasional y una revisión del diff en cada PR. Para el diff, los prompts 2, 3 y 4 funcionan bien porque son acotados.
❌ Confundir “no encontré nada” con “no hay nada”. Es el error más caro de todos, y por eso existe el prompt 10. Si no te dice explícitamente qué no ha mirado, asume que no lo ha mirado.
✅ Registrar la cobertura de cada auditoría. Anota qué módulos se han revisado y con qué fecha. Una auditoría sin fecha y sin alcance no es una auditoría, es una conversación.
❌ Pegar secretos al prompt. Mandas tu .env al modelo para que busque claves, y ahora la clave está en el historial de otra empresa.
✅ Pega la estructura, no los valores. En lugar de STRIPE_SECRET_KEY=sk_live_abc123, pega STRIPE_SECRET_KEY=${STRIPE_KEY} y pide que analice si el patrón es correcto, no el valor.
Errores frecuentes y cómo salir de ellos
El modelo se inventa funciones que no existen. Le da un resultado con authenticateUser() y tú no tienes esa función. Solución: dale el índice de ficheros (tree -L 2 src/) antes de que analice, y dile que pregunte por lo que no encuentre.
Todo sale como CRÍTICO. Un informe con veinte hallazgos críticos no se arregla, se ignora. Solución: pide siempre severidad con impacto real en tu aplicación, no la severidad genérica de OWASP. Un XSS reflected en un panel interno no es lo mismo que en una landing pública.
El análisis dura demasiado y se corta. Con repositorios grandes, parte el trabajo: un prompt por módulo, con un resumen de hallazgos al final. Perder contexto es mejor que perder el análisis entero.
Falsos positivos del tipo “esto es vulnerable porque usa http”. Pídele el escenario de explotación completo, no solo la categoría. Si no puede describir el camino completo desde el input hasta el sink, es un falso positivo.
Te propone arreglar algo rompiendo funcionalidad. Pide siempre el diff mínimo y la justificación de por qué no hay alternativa compatible. Si te dice “esto es mejor así” sin más, no lo apliques sin entender qué rompe.
No encuentra nada en archivos generados. Los ficheros de lock, los clientes generados y los .min.js no se auditan. Dile que los ignore, odoo que los revise por separado si son propios.
El informe se queda en un documento que nadie lee. Convierte los hallazgos en issues con severidad, esfuerzo y responsable. Un informe de seguridad sin backlog es un informe de seguridad perdido.
El flujo completo
┌───────────────────────────────────┐
│ Preparación │
│ · Qué rutas son públicas │
│ · Framework y versiones │
│ · Lista de ficheros a revisar │
│ · Reglas de privacidad (no secretos)│
└────────────────┬──────────────────┘
▼
┌───────────────────────────────────┐
│ Prompt 1 → OWASP Top 10 completo │ Barrido por taxonomía
└────────────────┬──────────────────┘
▼
┌─────────┬─────────┬──────────┐
▼ ▼ ▼ ▼
┌────────┐┌────────┐┌────────┐┌────────┐
│P.2 SQLi││P.3 XSS ││P.4 Sec.││P.5 AC │
│ ││ ││ ││ │
└───┬────┘└───┬────┘└───┬────┘└───┬────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐┌────────┐┌────────┐┌────────┐
│P.6 Val.││P.7 SSRF││P.8 Auth││P.9 Dep │
│ ││ ││ ││ │
└───┬────┘└───┬────┘└───┬────┘└───┬────┘
└─────────┴────┬────┴─────────┘
▼
┌──────────────────────────────┐
│ Prompt 10 → límites reales │ Qué no has visto
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ Verificación humana │ Ejecutar PoC una a una
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ Test de regresión por hallazgo│ Falla antes, pasa después
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ Lo que la IA no verá nunca │ Pentest + infra + proceso
└──────────────────────────────┘
Conclusión
Una IA es buena diccionario para detectar patrones conocidos y mala detective para descubrir vulnerabilidades nuevas. Usarla como primera pasada con prompts estructurados te ahorra horas de revisión manual y, sobre todo, te da un checklist que nadie se salta. Usarla como sustituto del pentest es donde empieza el problema.
Este artículo es la capa que se aplica sobre el código, no sobre los tests. Si quieres la estrategia completa, aquí están las otras tres piezas: los prompts para testear APIs REST cubren los contratos donde un test de autorización detecta el IDOR sin necesitar un atacante, los prompts para tests E2E con Playwright cubren los flujos completos donde aparecen los fallos de permisos encadenados, y los prompts para testear componentes React cubren la capa de renderizado donde un XSS se cuela. La estrategia general está en cómo testear código generado por IA.
Para el stack concreto donde se aplican estos hallazgos, tengo la guía de protección de API Node.js con JWT (que es el prompt 8 en forma de código correcto) y la implementación de webhooks de Stripe en Next.js, que es uno de los puntos de entrada más atacados que existe. La parte de dependencias va enlazada con mi guía de Docker para developers, donde la imagen base y las versiones fijadas son en sí mismas un vector de ataque.
Y un apunte que viene a cuento: los modelos alucinan con paquetes que no existen, lo cual es especialmente peligroso en un contexto de seguridad, porque puedes instalar algo malicioso confiando en un nombre que suena razonable. Tienes el detalle en IA alucina paquetes npm que no existen. Para el contexto general de herramientas, mira el ranking de modelos de IA para programar y mi comparativa Cursor vs Copilot vs Windsurf. Si quieres montar estos prompts en modo guiado, están en mi generador interactivo de prompts.