Str::mask() and Friends: Redacting Sensitive Data Before It Hits Your Logs
It is easy to log a request payload for debugging and forget it contains a full credit card number, an SSN, or a customer's email address in plain text. Laravel's Str::mask() helper makes it trivial to redact the sensitive part of a string before it ever reaches a log driver, without writing your own substring logic.
The basics
use Illuminate\Support\Str;
Str::mask('4242424242424242', '*', 4);
// '4242************'
Str::mask('taylor@laravel.com', '*', 3);
// 'tay****************'
The second argument is a positive index counting from the start, so 4 means "keep the first 4 characters, mask the rest."
Masking from the end
Pass a negative starting index to mask from the end of the string instead, which is the more common pattern for card numbers, where you want to show the last four digits and hide everything before them:
Str::mask('4242424242424242', '*', 0, -4);
// '************4242'
The fourth argument caps how many characters get masked, so here it masks everything except the final 4 characters.
Masking the middle of a string
Str::mask('taylor@laravel.com', '*', 3, -10);
// 'tay**********m.com'
Combining a positive start and negative length lets you keep both ends visible while hiding the middle, handy for things like partially redacted emails in an admin support panel.
Applying it before logging a request payload
namespace App\Http\Middleware;
use Illuminate\Support\Str;
class LogSanitizedRequest
{
public function handle($request, \Closure $next)
{
$payload = $request->all();
if (isset($payload['card_number'])) {
$payload['card_number'] = Str::mask($payload['card_number'], '*', 0, -4);
}
if (isset($payload['ssn'])) {
$payload['ssn'] = Str::mask($payload['ssn'], '*', 0, -4);
}
logger()->info('Incoming payment request', $payload);
return $next($request);
}
}
Don't forget your log channel's own scrubbing
Str::mask() handles values you explicitly choose to redact, but it's worth pairing with a broader safety net. Laravel's logging config lets you register a custom formatter or processor on a channel to catch fields you might miss by hand:
// config/logging.php
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single'],
'tap' => [\App\Logging\RedactSensitiveFields::class],
],
],
Why this deserves five minutes of your time
Every team has a story about a card number, password, or auth token showing up in a log aggregator because someone logged $request->all() during a debugging session and forgot to remove it before merging. Str::mask() costs one line at the point you log something sensitive, and it turns "we have PCI-relevant data sitting in Papertrail" into a non-event. Given how cheap it is to apply, there's not much reason for a payment or auth-adjacent code path to skip it.