OWASP LLM Top 10 en producción: cómo audité mi pipeline de agentes TypeScript contra los 10 riesgos y qué encontré
OWASP LLM Top 10 en producción: cómo audité mi pipeline de agentes TypeScript contra los 10 riesgos y qué encontré
Estaba revisando un system prompt de un agente MCP que había escrito tres semanas antes cuando me di cuenta de algo perturbador: el prompt aceptaba instrucciones de la respuesta de una tool externa. Sin sanitización. Sin validación. Sin ningún límite sobre qué podía hacer con esa salida. La tool llamaba a una API pública, recibía JSON, y ese JSON llegaba directo al contexto del modelo.
Ahí fue cuando abrí el OWASP LLM Top 10 y paré de leerlo como lista de buenas prácticas para empezar a usarlo como lo que en realidad es: un framework de auditoría.
Mi tesis es esta: la mayoría de los posts sobre OWASP LLM Top 10 te explican los diez riesgos. Ninguno te muestra cómo correrlos contra tu stack propio y qué encontrás cuando lo hacés en serio. Esa es la diferencia entre "leer el checklist" y "auditar el pipeline". Acá está lo segundo.
El stack que audité y por qué importa el contexto
Antes de entrar al checklist, el contexto: tengo un pipeline de agentes en TypeScript con tres capas que interactúan:
- System prompts estructurados - instrucciones que definen el comportamiento del agente, separadas del contexto de usuario
- MCP tools - tools registradas siguiendo el Model Context Protocol, que el agente puede llamar durante una sesión
- Cline como cliente - que orquesta la ejecución en el editor y tiene acceso a filesystem, terminal y otras herramientas
Cada capa tiene una superficie de ataque diferente. Eso es lo que el OWASP LLM Top 10 me permitió ver con precisión quirúrgica.
Los 10 riesgos: qué encontré en cada uno
LLM01 - Prompt Injection
Este fue el hallazgo más gordo. Mi agente MCP recibía output de tools externas y lo incorporaba al contexto sin ninguna capa de sanitización. En un escenario adversarial, cualquier API que el agente consultara podría devolver texto diseñado para sobrescribir las instrucciones del system prompt.
El patrón roto era este:
// ❌ Patrón inseguro: output externo directo al contexto
async function fetchContextAndInject(url: string): Promise<string> {
const response = await fetch(url);
const data = await response.json();
// data.content llega sin ningún filtro al contexto del modelo
return data.content;
}
Lo que cambié:
// ✅ Validación de estructura antes de incorporar al contexto
import { z } from "zod";
const ExternalResponseSchema = z.object({
// Solo acepto campos con tipo definido - string libre marcado como sospechoso
title: z.string().max(200),
summary: z.string().max(1000),
// Descarto cualquier campo que no esté en el schema
});
async function fetchContextSafe(url: string): Promise<string> {
const response = await fetch(url);
const raw = await response.json();
// Si el schema falla, el agente recibe un error estructurado, no el payload crudo
const parsed = ExternalResponseSchema.parse(raw);
return `Título: ${parsed.title}\nResumen: ${parsed.summary}`;
}
Usé Zod - que ya tenía en el stack para validación de API - como primera línea. No es una solución completa al prompt injection, pero reduce la superficie de ataque estructural.
LLM02 - Insecure Output Handling
El segundo problema: el output del agente llegaba a la UI sin escaping. En un agente que genera HTML o Markdown, eso es XSS potencial si el output se renderiza directamente. Revisé dónde el output del modelo llegaba al DOM y agregué sanitización explícita antes de cualquier render. Si el agente genera código, ese código va a un bloque <pre> con escape de caracteres; no a un innerHTML.
LLM03 - Training Data Poisoning
Acá el OWASP LLM Top 10 apunta a riesgos del modelo base, no de la aplicación. En mi caso el modelo es Claude vía API - no controlo el fine-tuning ni el dataset. Mi única acción fue documentar esta dependencia explícitamente: si Anthropic tiene un problema acá, yo tengo un problema acá. Ningún system prompt lo compensa.
Límite honesto: no podés auditar esto desde la aplicación. Es una dependencia que tomás como trust boundary.
LLM04 - Model Denial of Service
Revisé si tenía rate limiting en los endpoints que disparan llamadas al modelo. No lo tenía en el contexto de pruebas locales. En un escenario de producción esto es crítico: un loop mal diseñado o una tool que llama recursivamente puede generar decenas de requests al modelo en segundos.
Agregué un límite simple de iteraciones al loop del agente:
// Control de iteraciones para evitar loops infinitos en el agente
const MAX_ITERATIONS = 10;
let iterations = 0;
while (agentShouldContinue && iterations < MAX_ITERATIONS) {
iterations++;
const result = await runAgentStep();
agentShouldContinue = result.continueLoop;
}
if (iterations >= MAX_ITERATIONS) {
// Log explícito - quiero saber si esto se dispara
console.warn("[agente] Límite de iteraciones alcanzado - revisar loop");
}
LLM05 - Supply Chain Vulnerabilities
Este riesgo me hizo revisar dos cosas: los paquetes npm que uso para interactuar con la API del modelo y las dependencias de mis MCP tools. Con pnpm workspaces (tema que ya cubrí en el post de monorepo con Railway) tenés visibilidad del lockfile, pero eso no es auditoría. Lo que agregué: pnpm audit como paso explícito en CI antes del deploy de cualquier agente. No elimina el riesgo, pero lo hace visible.
LLM06 - Sensitive Information Disclosure
Acá encontré el segundo hallazgo incómodo: en los system prompts tenía contexto de configuración que incluía nombres de tools internas, estructura de datos y algunos defaults del sistema. Ese contexto llega al modelo - y si el modelo lo repite en su output, lo expone.
La regla que apliqué: nada que no quieras ver en un log público debería estar en un system prompt sin marcado explícito de confidencialidad. Y eso tampoco es garantía - es mitigación.
// Separar configuración técnica de instrucciones del agente
const SYSTEM_PROMPT_PUBLIC = `
Sos un asistente de desarrollo. Podés usar las herramientas disponibles para responder preguntas técnicas.
`;
// Esto NO va al system prompt - va a una capa de configuración separada
const AGENT_CONFIG_PRIVATE = {
toolEndpoints: process.env.TOOL_ENDPOINTS,
internalSchema: process.env.INTERNAL_SCHEMA,
};
LLM07 - Plugin Design Flaws
Mis MCP tools son básicamente plugins. El riesgo acá es que una tool tenga permisos más amplios de lo necesario. Revisé cada tool y apliqué el principio de mínimo privilegio: una tool que lee archivos no necesita escribir; una tool que consulta una API no necesita acceso al filesystem. Esto conecta con lo que escribí sobre OAuth scope creep - el mismo patrón de auditoría aplica a las tools de un agente.
LLM08 - Excessive Agency
Este es el riesgo que más me preocupa en Cline específicamente. El agente tiene acceso a terminal, puede ejecutar comandos, puede modificar archivos. Si el loop de razonamiento falla, puede hacer daño real.
Lo que implementé: modo "confirm before execute" para cualquier tool con efecto secundario irreversible. No es automatizable - requiere fricción humana deliberada. Y esa fricción es el punto.
// Clasificación explícita de tools por impacto
type ToolImpact = "read-only" | "reversible" | "destructive";
const TOOL_IMPACT_MAP: Record<string, ToolImpact> = {
readFile: "read-only",
listDirectory: "read-only",
writeFile: "reversible",
deleteFile: "destructive",
runCommand: "destructive",
};
async function executeTool(toolName: string, args: unknown) {
const impact = TOOL_IMPACT_MAP[toolName] ?? "destructive"; // fallback seguro
if (impact === "destructive") {
// Pausa y espera confirmación humana antes de ejecutar
await requireHumanApproval(toolName, args);
}
return runTool(toolName, args);
}
LLM09 - Overreliance
No es un riesgo técnico puro - es organizacional. El problema es confiar en el output del agente sin validación externa. En mi pipeline, cualquier output que va a producción pasa por una capa de validación estructural antes de ser usado como input de otro sistema. El modelo puede estar seguro, el pipeline puede estar seguro, y el output puede seguir siendo incorrecto. Este riesgo no se cierra con código. Se cierra con proceso y revisión humana en los nodos críticos.
LLM10 - Model Theft
En mi contexto de agente TypeScript, esto aplica principalmente a la protección de los system prompts. Un system prompt elaborado representa trabajo real - y si se expone, puede ser replicado o usado para evadir restricciones. Lo que implementé: los system prompts no viven en el código del frontend. Se sirven desde un endpoint autenticado, no se loguean en texto plano y no se exponen en el bundle del cliente.
Lo que el OWASP LLM Top 10 no te dice (y es igual de importante)
Acá está lo que la lista no resuelve sola:
- No te dice el orden de prioridad para tu stack. LLM01 (prompt injection) fue crítico en mi caso; LLM03 (training data poisoning) es irrelevante desde la aplicación. Sin aplicarlo contra tu arquitectura concreta, no sabés cuál es urgente.
- No te da criterio para el trust boundary del modelo base. Si usás Claude, GPT-4 o cualquier API externa, LLM03 y parte de LLM05 son dependencias que tomás como dadas. El framework las nombra, pero la mitigación no está en tus manos.
- No distingue entre riesgos de runtime y riesgos de diseño. LLM01 y LLM02 son problemas que podés detectar y mitigar en runtime. LLM08 (excessive agency) es un problema de diseño - si el agente tiene demasiados permisos, un patch de runtime no lo arregla.
Tengo un post sobre OpenTelemetry en Next.js donde hablo de traces que sobreviven el edge. Ese tipo de observabilidad también ayuda acá: si no podés ver qué tools llamó el agente y con qué args, no podés auditar LLM08 en producción.
Checklist aplicado: el estado real de cada riesgo en mi pipeline
| Riesgo | Estado encontrado | Acción tomada |
|---|---|---|
| LLM01 Prompt Injection | ❌ Vulnerable | Zod schema en output de tools externas |
| LLM02 Insecure Output | ⚠️ Parcial | Escaping explícito antes de render |
| LLM03 Training Data | 🔵 Fuera de scope | Documentado como trust boundary |
| LLM04 Model DoS | ⚠️ Sin límite | Agregué max iterations + log |
| LLM05 Supply Chain | ⚠️ Invisible | pnpm audit en CI |
| LLM06 Info Disclosure | ❌ Leaky prompts | Separé config de system prompt |
| LLM07 Plugin Flaws | ⚠️ Parcial | Revisión de permisos por tool |
| LLM08 Excessive Agency | ⚠️ Sin fricción | Confirm before execute en tools destructivas |
| LLM09 Overreliance | 🔵 Proceso | Validación humana en nodos críticos |
| LLM10 Model Theft | ⚠️ Prompts expuestos | Prompts a endpoint autenticado |
❌ = hallazgo crítico | ⚠️ = mitigación parcial | 🔵 = fuera del control de la aplicación
FAQ
¿El OWASP LLM Top 10 aplica a agentes basados en Claude o GPT-4 vía API?
Sí, con matices. LLM01, LLM02, LLM06, LLM07, LLM08 y LLM10 son riesgos de la aplicación - aplican sin importar qué modelo uses. LLM03 (training data) y parte de LLM05 son riesgos del proveedor: si usás una API externa, los tomás como trust boundary. La auditoría empieza por los riesgos que sí podés controlar.
¿Con Zod alcanza para mitigar prompt injection?
No. Zod valida la estructura del output externo antes de que llegue al contexto - eso reduce la superficie, pero no elimina el riesgo. Un payload adversarial bien formado puede pasar la validación de schema. Zod es una capa, no una solución completa. La mitigación real combina schema validation, restricciones en el system prompt y revisión humana en puntos críticos.
¿Cline es seguro para usar en producción como orquestador de agentes?
Cline tiene acceso a filesystem, terminal y otras herramientas con efecto real. Eso no es inherentemente inseguro - es la funcionalidad que lo hace útil. El riesgo (LLM08) está en el diseño: si el agente puede ejecutar comandos destructivos sin confirmación humana, el riesgo es real independientemente de qué tan bien esté configurado Cline. La regla que aplico: cualquier tool con efecto irreversible requiere aprobación explícita.
¿Cada cuánto hay que correr esta auditoría?
Cada vez que cambiás la arquitectura del agente: agregás una tool nueva, cambiás el system prompt o modificás cómo el agente consume outputs externos. No es una auditoría de una sola vez - es un checklist que corre contra cada cambio estructural. Si agregás observabilidad (OpenTelemetry es una opción), podés detectar anomalías en runtime entre auditorías.
¿El OWASP LLM Top 10 cubre riesgos de multi-agente o solo agente único?
La versión actual (2025) cubre principalmente el riesgo por agente. En arquitecturas multi-agente, la superficie de LLM01 se multiplica: cada agente puede ser un vector de inyección para los demás. El framework nombra el riesgo, pero el detalle de mitigación para pipelines multi-agente queda en manos de cada equipo.
¿Qué riesgo debería atacar primero si tengo tiempo limitado?
LLM01 (prompt injection) si tu agente consume output externo - es el más explotable y el más ignorado. LLM08 (excessive agency) si el agente tiene acceso a herramientas con efecto irreversible - es el que más daño puede hacer en un fallo. Los demás dependen de tu stack, pero estos dos son el piso mínimo.
Conclusión: la diferencia entre leer y auditar
Mi postura es clara: el OWASP LLM Top 10 no sirve para leerlo y darlo por cubierto. Sirve para llevarlo a una sesión de revisión con el diagrama de arquitectura enfrente y preguntar, por cada riesgo, dónde exactamente en el pipeline eso puede fallar.
Lo que no compro es la idea de que "seguir las buenas prácticas" alcanza. Las prácticas son abstractas; el pipeline es concreto. En mi caso, LLM01 y LLM06 eran problemas reales que no habría encontrado sin hacer el ejercicio de auditoría sistemática. Los habría descubierto cuando alguien con motivación los explotara.
Si ya tenés agentes en TypeScript con MCP tools o system prompts elaborados, hacé el ejercicio: abrí el OWASP LLM Top 10, abrí el diagrama de arquitectura y preguntá riesgo por riesgo. El resultado va a ser más interesante que el listado.
Próximo paso concreto: tomá el checklist.
Comments
No comments yet. Start the discussion.