
Your Laravel app looks production-ready. Tests pass. Deployment is smooth. But somewhere in your codebase, sensitive data is waiting to leak—through logs, error pages, or that innocent-looking API response.
Security breaches rarely come from clever hacks. They come from overlooked defaults and lazy configurations.
Let's fix that.
The Hidden Dangers in Your .env
Every Laravel developer knows to set APP_DEBUG=false in production. But how many check these?
1# The silent killers
2LOG_CHANNEL=stack
3LOG_LEVEL=debug
4APP_DEBUG=false
That LOG_LEVEL=debug is still writing every SQL query, every request payload, every exception with full stack traces—including user passwords in plain text when authentication fails.
The Fix
1LOG_LEVEL=error
Better yet, use a log channel that scrubs sensitive data:
1// config/logging.php
2'secure' => [
3 'driver' => 'stack',
4 'channels' => ['daily'],
5 'tap' => [\App\Logging\ScrubSensitiveData::class],
6],
Mass Assignment: Still Dangerous in 2026
Laravel's $fillable and $guarded have been around forever. Yet mass assignment vulnerabilities still top the OWASP charts for Laravel apps.
The problem isn't knowledge—it's laziness.
1// The lazy way (vulnerable)
2protected $guarded = [];
3
4// The tired way (error-prone)
5protected $fillable = ['name', 'email', 'password'];
6// Wait, did we add is_admin later?
The 2026 Approach
Use DTOs and explicit assignment:
1// Create a strict DTO
2class CreateUserData
3{
4 public function __construct(
5 public readonly string $name,
6 public readonly string $email,
7 public readonly string $password,
8 ) {}
9}
10
11// In your controller
12$data = new CreateUserData(...$request->validated());
13User::create((array) $data);
No more guessing which fields are fillable. Explicit beats implicit.
API Responses: The Oversharing Problem
Your User model probably has a toArray() that returns everything. Including password, remember_token, and that ssn column you added for enterprise clients.
1// This endpoint
2return response()->json(User::find($id));
3
4// Returns this
5{
6 "id": 1,
7 "name": "John Doe",
8 "email": "john@example.com",
9 "password": "$2y$10$...", // Oops
10 "remember_token": "...", // Double oops
11 "created_at": "..."
12}
The Fix: API Resources (Always)
1class UserResource extends JsonResource
2{
3 public function toArray($request): array
4 {
5 return [
6 'id' => $this->id,
7 'name' => $this->name,
8 'email' => $this->when(
9 $request->user()?->is($this->resource),
10 $this->email
11 ),
12 ];
13 }
14}
Never return raw models from API endpoints. Period.
Rate Limiting: Your First Line of Defense
Laravel's rate limiting is powerful but underused. Most apps only limit login attempts.
1// routes/api.php - The minimum
2Route::middleware(['auth:sanctum', 'throttle:60,1'])
3 ->group(function () {
4 // Your routes
5 });
But smart attackers target specific endpoints:
1// Protect expensive operations
2RateLimiter::for('exports', function (Request $request) {
3 return Limit::perHour(5)->by($request->user()->id);
4});
5
6RateLimiter::for('password-reset', function (Request $request) {
7 return Limit::perMinute(3)->by($request->ip());
8});
SQL Injection: It's Not Just About Escaping
Yes, Eloquent escapes queries. No, you're not safe.
1// This looks safe (it's not)
2$users = User::whereRaw("role = '$role'")->get();
3
4// This too
5$users = User::orderBy($request->input('sort'))->get();
The second example allows arbitrary column exposure through error messages.
The Fix
1// Validate and whitelist
2$sortable = ['name', 'created_at', 'email'];
3$sort = in_array($request->input('sort'), $sortable)
4 ? $request->input('sort')
5 : 'created_at';
6
7$users = User::orderBy($sort)->get();
Headers: The Forgotten Security Layer
Your responses need security headers. Not optional.
1// app/Http/Middleware/SecurityHeaders.php
2public function handle($request, Closure $next)
3{
4 $response = $next($request);
5
6 return $response
7 ->header('X-Content-Type-Options', 'nosniff')
8 ->header('X-Frame-Options', 'DENY')
9 ->header('X-XSS-Protection', '1; mode=block')
10 ->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
11 ->header('Content-Security-Policy', "default-src 'self'")
12 ->header('Permissions-Policy', 'geolocation=(), microphone=()');
13}
Add it to your global middleware stack.
The 5-Minute Security Audit
Run this checklist before every deployment:
Beyond the Basics
For production apps handling sensitive data:
- Encrypt at rest: Use Laravel's
$casts with encrypted attributes
- Audit logging: Track who accessed what, when
- 2FA: Mandatory for admin accounts
- Dependency scanning: Run
composer audit in CI
- Penetration testing: Hire professionals annually
Ship Secure
Security isn't a feature you add at the end. It's a habit you build from the first commit.
The vulnerabilities in this guide are embarrassingly common. They exist in production apps right now—maybe even yours.
Take 10 minutes today. Run the checklist. Fix one thing. Then another tomorrow.
Your users trust you with their data. Earn it.
A practical security guide from the Beartropy team