Consultas y vistas en el código generado por IA: whereRaw y HTML sin escapar
Hola, bienvenido. En esta lección vamos a corregir 2 sitios de invoices en los que el texto que escribe el usuario acaba sin escapar dentro de otro código. El primero es la búsqueda de facturas, que pone lo que escribes en el buscador dentro de una consulta SQL con whereRaw, y el segundo son las notas de la factura, que se pintan dentro del HTML de la página con {!! !!}. Vamos a escribir un test para cada uno con entradas inofensivas, a aplicar la solución con whereLike y con e(), y a ver por qué ni Larastan ni Pint podían avisarnos.
Estos 2 fallos son del segundo grupo que vimos en la lección 1. En las lecciones 6 y 7 corregíamos comprobaciones que nadie había escrito, una policy que no se llamaba o un form request que no existía. Aquí el código sí está, hace lo que pide la especificación y falla en un detalle.
El texto del usuario dentro de SQL y de HTML
Partimos de invoices tal como lo dejamos en la lección anterior. Esta es la línea que filtra el listado de facturas en InvoiceController@index:
Y esta es la que pinta las notas en resources/views/invoices/show.blade.php:
Las 2 hacen lo mismo con lenguajes distintos: juntan un texto que viene del usuario con un código que después interpreta otro programa. En la búsqueda, el texto se pega dentro de la consulta y MySQL recibe una sola cadena de SQL. Si lo que escribe el usuario lleva una comilla simple ('), esa comilla cierra la cadena del LIKE y lo que venga detrás ya no se lee como texto, sino como parte de la consulta. Eso es la inyección SQL. En las notas pasa lo mismo con el navegador: si el texto lleva etiquetas HTML, el navegador las interpreta, y si alguna es un script, se ejecuta en el navegador de quien abre la factura, con su sesión. Eso es el cross-site scripting, XSS. Las 2 están dentro de A05, Injection, en el OWASP Top 10:2025.
Laravel protege las 2 cosas por defecto. El query builder pasa los valores a la base de datos por separado de la consulta, con los bindings de PDO, y las llaves dobles de Blade, {{ }}, escapan lo que pintan con htmlspecialchars. whereRaw y {!! !!} son justo las 2 formas de saltarse esa protección, y la documentación de Laravel lo avisa en los 2 casos. De las consultas dice que Laravel no puede garantizar que una consulta con expresiones en bruto esté protegida contra la inyección SQL, y de Blade, que tengas mucho cuidado al pintar sin escapar contenido que escriben los usuarios.
¿Y por qué un agente escribe esto? Por la especificación. Pedía la búsqueda por número "sin distinguir mayúsculas", y LOWER(...) LIKE es la receta de SQL de toda la vida para eso; con whereRaw, el agente la escribe tal como la escribiría en la consola de MySQL. Las notas tenían que conservar "los saltos de línea al mostrarse". nl2br() convierte cada salto en un <br />, pero con {{ nl2br(...) }} Blade escapa también esos <br /> y en la página se ve el texto de la etiqueta. El agente cambia a {!! !!} y los saltos aparecen. Es el mismo patrón que $guarded = [] en la lección anterior: la versión que funciona a la primera.
Y los tests del agente pasan, porque usan entradas normales. InvoiceTest busca f-2026-0001, en minúsculas, y encuentra F-2026-0001, y comprueba que las notas "Pago a 30 días." aparecen en la factura. Con esas entradas las 2 líneas funcionan perfectamente.
- 02Los tests: una comilla simple en la búsqueda y una etiqueta en las notas
- 03La solución: whereLike y e()
- 04Por qué Larastan y Pint no lo ven