Saltar al contenido
Volver al blog
LaravelSeguridadBuenas prácticas

Laravel 13.27 mejora las consultas sensibles y los bloqueos

Tres cambios pequeños de Laravel 13.27 que reducen filtraciones en errores y simplifican código con concurrencia.

Ismael Catala5 min de lectura

Cuando una excepción enseña más de la cuenta

Una excepción de base de datos puede acabar en logs, en una plataforma de observabilidad o en una tabla de trabajos fallidos. Si su mensaje incorpora los valores ligados a la consulta, también puede arrastrar correos, identificadores, datos personales o cualquier otro valor que hubiera llegado a la query. Laravel 13.27 añade una opción por conexión para evitar esa interpolación en el mensaje de QueryException. La clave exacta es mask_bindings_in_exception_messages y su valor por defecto es false. (raw.githubusercontent.com)

Yo activaría esta opción en las conexiones que usen datos reales, especialmente en producción. No sustituye una política de logs, pero elimina una vía bastante habitual por la que información de entrada termina en sistemas donde no debería estar. Además, Laravel incluye la opción en su configuración de base de datos por defecto, así que puede controlarse mediante una variable de entorno. (raw.githubusercontent.com)

// config/database.php
 
'mysql' => [
    'driver' => 'mysql',
    // ...
    'mask_bindings_in_exception_messages' => env('DB_MASK_BINDINGS', false),
],

Con DB_MASK_BINDINGS=true, la consulta seguirá apareciendo en el mensaje de error, pero los valores permanecerán como marcadores ?. Esto conserva contexto suficiente para diagnosticar qué sentencia falló sin imprimir automáticamente cada binding. Es un cambio de configuración pequeño y razonable para revisar antes de que el siguiente error de integridad llegue a un servicio externo. (github.com)

Comparaciones que no dependen de la collation

Otra incorporación es whereBinary(), disponible en el query builder. Sirve para hacer comparaciones byte a byte en MySQL y MariaDB sin recurrir a whereRaw(). También están disponibles orWhereBinary(), whereNotBinary() y orWhereNotBinary(). (raw.githubusercontent.com)

Esto es útil cuando una columna de texto usa una collation que no distingue mayúsculas y minúsculas, pero una consulta concreta sí debe hacerlo. Un ejemplo razonable es tratar un nombre técnico, una clave externa o un identificador con semántica sensible a mayúsculas de forma estricta. La intención queda más clara que con SQL embebido y el valor sigue viajando como binding. (github.com)

$queue = DB::table('queues')
    ->whereBinary('name', $queueName)
    ->first();
 
$otherQueues = DB::table('queues')
    ->whereNotBinary('name', $queueName)
    ->get();

No usaría whereBinary() como sustituto automático de todos los where(). Es una decisión de semántica de datos: hay que saber si el valor debe diferenciar realmente entre Admin, admin y ADMIN. Si la regla pertenece al modelo de dominio, conviene acompañarla de validación, restricciones y pruebas que dejen claro por qué esa diferencia importa.

Recargar y bloquear la fila correcta

refreshForUpdate() resuelve un patrón repetido en código de stock, reservas o tareas que modifican un recurso compartido. Cuando ya tienes una instancia de Eloquent, el método la recarga desde la base de datos y aplica lockForUpdate() a esa lectura. La instancia se actualiza en el sitio, por lo que no hace falta reasignarla a otra variable. (raw.githubusercontent.com)

El caso típico es recibir un modelo mediante route model binding y abrir después una transacción. Antes había que construir una consulta nueva por clave primaria para obtener una versión actualizada y bloqueada de la fila. Ahora el flujo puede quedar así. (github.com)

use Illuminate\Support\Facades\DB;
use RuntimeException;
 
DB::transaction(function () use ($product) {
    $product->refreshForUpdate();
 
    if ($product->stock <= 0) {
        throw new RuntimeException('No hay stock disponible.');
    }
 
    $product->decrement('stock');
});

La mejora aquí no es que el bloqueo haga magia, sino que reduce código accidental alrededor de una operación delicada. La lectura actualizada y la modificación deben formar parte de la misma transacción para que el bloqueo pesimista tenga sentido. Laravel recomienda envolver los bloqueos pesimistas en una transacción para que se liberen al completar la operación. (laravel.com)

La letra pequeña

El enmascarado no borra bindings de todas partes. La conexión sigue emitiendo los bindings en el evento QueryExecuted y puede guardarlos en el query log si este está activado; además, las APIs que recuperan bindings siguen teniendo acceso a los valores. Hay que revisar listeners, herramientas APM, trazas y cualquier middleware propio que registre solicitudes o consultas. (raw.githubusercontent.com)

whereBinary() tampoco es una abstracción portátil entre motores. Laravel lo compila para MySQL y MariaDB, mientras que los demás motores lanzan una RuntimeException; si la aplicación debe funcionar sobre SQLite en pruebas y MySQL en producción, esta diferencia debe estar cubierta por tests o encapsulada detrás de una decisión explícita. (github.com)

Con refreshForUpdate() hay una advertencia menos visible: la recarga usa una consulta sin scopes globales y busca el registro por su clave. También utiliza firstOrFail(), de modo que puede fallar si la fila ya no existe cuando se intenta recargar. No lo trataría como un simple refresh() de comodidad, sino como una operación de concurrencia que merece pruebas de integración con la base de datos real. (raw.githubusercontent.com)

Laravel 13.27 no cambia por completo cómo se diseñan operaciones concurrentes ni cómo se gobiernan los logs. Sí elimina tres pequeñas fuentes de código frágil: SQL raw para comparaciones exactas, recargas manuales antes de bloquear y mensajes de excepción demasiado expresivos. Para mí, son mejoras que merece la pena aplicar donde el código toca datos sensibles o recursos con disponibilidad limitada. (github.com)


Fuente: Laravel News Documentación oficial: Laravel Framework v13.27.0