La respuesta del modelo es entrada no fiable: escapar, validar y testear con fakes
Hola, bienvenido. En esta lección vamos a tratar lo que responde el modelo igual que tratamos lo que escribe un usuario. La respuesta de un modelo de lenguaje es texto que no controlas, y puede llevar etiquetas HTML, rutas o datos que no son ciertos. Vamos a pintar en la página del asistente las respuestas en markdown sin abrir un XSS y sin que una imagen de la respuesta pueda mandar datos a otro servidor, con tests que lo comprueban. Después veremos cómo se valida una salida estructurada antes de usarla, y cerraremos la sección con qué se puede comparar letra a letra en un test y qué no.
La respuesta del modelo no es de fiar
El OWASP Top 10 for LLM Applications 2025 le dedica una categoría, LLM05:2025, Improper Output Handling, el tratamiento inadecuado de la salida. La define como la falta de validación, saneamiento y tratamiento de lo que genera el modelo antes de pasarlo a otros componentes. Y explica por qué es un riesgo: el contenido que genera el modelo se puede controlar desde el prompt, así que es como darle al usuario un acceso indirecto a más funciones de la aplicación.
En invoices ya lo hemos visto por los 2 lados. En la lección 17, unas notas escritas por otra persona decidían con qué id pedía el modelo una factura. Lo que escribe el modelo puede llevar lo que alguien puso en un texto que ha leído. Y en la lección anterior, en una de nuestras pruebas, el asistente contestó que una factura estaba pagada sin haber llamado a la tool. El texto decía una cosa y la base de datos, otra.
La primera recomendación de OWASP para LLM05 es esta: "Treat the model as any other user, adopting a zero-trust approach, and apply proper input validation on responses coming from the model to backend functions". Es decir, tratar al modelo como a cualquier otro usuario, sin confiar en él, y validar sus respuestas antes de pasarlas a cualquier función de la aplicación. En Laravel, eso se traduce en las mismas reglas que ya aplicamos al texto del usuario en la sección 2, según adónde vaya la respuesta:
| Adónde llega la respuesta | Qué puede pasar | Cómo se trata |
|---|---|---|
| Una vista de Blade | XSS | Con {{ }}, y nunca {!! !!} sobre una respuesta sin escapar |
whereRaw o DB::raw |
Inyección SQL | El query builder con bindings, como en la lección 8 |
Process |
Ejecutar comandos en el servidor | Nunca se construye un comando con la respuesta |
redirect() |
Una redirección a otro sitio | Solo a rutas con nombre de la aplicación |
Storage::download() |
Leer archivos fuera de su carpeta | El archivo sale de un modelo y su policy, nunca de una ruta que escribe el modelo |
Todos menos la redirección son ejemplos que da la propia ficha de LLM05: la respuesta que acaba en una shell, el JavaScript o el markdown que interpreta el navegador, la consulta SQL sin parámetros y la ruta de archivo construida con la respuesta. En invoices, la respuesta del modelo solo llega a 2 sitios: a la página del asistente, como texto, y a las tools, como argumentos. Los argumentos ya los validan las tools con $request->validate() y la policy desde las lecciones 17 y 18. Nos queda la página.
- 02Las respuestas en markdown
- 03El test: markdown sí, HTML y enlaces inseguros no
- 04La solución: Str::markdown con sus opciones de seguridad
- 05Las imágenes del markdown
- 06Salida estructurada: validar antes de usar
- 07Qué se compara letra a letra y qué no