The Problem: Bound Values End Up Everywhere
When a database query fails in Laravel, the framework throws a QueryException whose message contains the full SQL statement with every bound value interpolated inline. This is intentional — a message like SQL: insert into "users" ("email") values (?) tells you almost nothing about what went wrong, while the interpolated version pinpoints the offending row immediately.
The trouble is that exception messages do not stay in one place. Consider this realistic QueryException message:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'ada@example.com'
for key 'users_email_unique' (Connection: mysql, SQL: insert into `users`
(`email`, `name`, `national_id`) values (ada@example.com, Ada Lovelace, 640312-4185))
Every bound value — an email address, a full name, a government identifier — is now a plain string inside an exception. That string travels to:
- Application log files written by your logging stack.
- The
failed_jobstable, becauseDatabaseFailedJobProvider::log()casts the exception to a string before inserting it. - APM and OpenTelemetry agents, which record the exception on the active span.
- Any third-party error-reporting service your application sends exceptions to.
Anywhere exceptions are persisted or transmitted, a copy of those bindings now lives.
The Fix: mask_bindings_in_exception_messages
Laravel 13.27 introduces a per-connection configuration key that stops the interpolation before the message is built.
Enabling It in config/database.php
'connections' => [
'mysql' => [
'driver' => 'mysql',
// ...
'mask_bindings_in_exception_messages' => env('DB_MASK_BINDINGS', false),
],
],
The key is already present in the framework's own config/database.php for all five default connections. If your application has never published that file, you do not need to publish it — just set the environment variable:
DB_MASK_BINDINGS=true
What the Message Looks Like After Masking
With the option enabled, bound values are replaced by their original ? placeholders:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'ada@example.com'
for key 'users_email_unique' (Connection: mysql, SQL: insert into `users`
(`email`, `name`, `national_id`) values (?, ?, ?))
The database error itself (including the duplicate-entry value surfaced by MySQL) is still present, but none of the application-supplied bindings appear in the message.
Key Takeaways
- Laravel's
QueryExceptioninterpolates bound values into its message by default, which can expose PII in logs,failed_jobs, and observability tooling. - Laravel 13.27 adds
mask_bindings_in_exception_messagesas a per-connection option inconfig/database.php. - Setting
DB_MASK_BINDINGS=trueis sufficient for applications that have not published the database config file. - Masking is opt-in and per-connection, so you can apply it selectively to connections that handle sensitive data.
- The SQL structure and the database-level error message remain intact; only the application-supplied binding values are withheld.
Source: Mask Query Bindings in Laravel Exception Messages — Laravel News