LARAVEL SECURITY BACKEND PHP

Laravel Security Essentials: Beyond the Defaults

⏱️ 7 min read
👨‍💻

Laravel Security Essentials: Beyond the Defaults

Laravel ships with excellent security defaults, but production applications need additional layers of protection. Here’s what I’ve learned from securing Laravel apps in high-stakes environments.

The Security Onion Model

Think of security as an onion—multiple layers that protect your application core:

  1. Network Level - Firewalls, VPNs, CDN
  2. Application Level - Authentication, authorization, validation
  3. Database Level - Encryption, access controls
  4. Code Level - Secure coding practices

Beyond Basic Authentication

Multi-Factor Authentication (MFA)

// Using Laravel Fortify with custom MFA
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;

class CustomTwoFactorAuth
{
    public function enable(Request $request)
    {
        $user = $request->user();

        // Generate backup codes
        $backupCodes = collect(range(1, 8))
            ->map(fn () => Str::random(10))
            ->toArray();

        $user->forceFill([
            'two_factor_recovery_codes' => encrypt(json_encode($backupCodes)),
        ])->save();

        // Your MFA logic here
    }
}

JWT with Refresh Tokens

class JWTAuthService
{
    public function createTokenPair($user)
    {
        $accessToken = $this->createAccessToken($user);
        $refreshToken = $this->createRefreshToken($user);

        // Store refresh token hash in database
        $user->refresh_tokens()->create([
            'token_hash' => hash('sha256', $refreshToken),
            'expires_at' => now()->addDays(30),
        ]);

        return compact('accessToken', 'refreshToken');
    }
}

Database Security Patterns

Attribute Encryption

use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model
{
    protected function socialSecurityNumber(): Attribute
    {
        return Attribute::make(
            get: fn ($value) => decrypt($value),
            set: fn ($value) => encrypt($value),
        );
    }
}

Query Builder Security

// ❌ Vulnerable to injection
DB::select("SELECT * FROM users WHERE role = '{$role}'");

// ✅ Parameterized queries
DB::select('SELECT * FROM users WHERE role = ?', [$role]);

// ✅ Even better with Query Builder
User::where('role', $role)->get();

API Security Deep Dive

Rate Limiting Strategies

// Custom rate limiter in RouteServiceProvider
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by(
        $request->user()?->id ?: $request->ip()
    )->response(function () {
        return response()->json([
            'message' => 'Too many requests. Please slow down.'
        ], 429);
    });
});

CORS Configuration

// config/cors.php
return [
    'paths' => ['api/*'],
    'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
    'allowed_origins' => [
        'https://yourapp.com',
        'https://admin.yourapp.com',
    ],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => true,
];

Input Validation & Sanitization

Custom Validation Rules

class SecurePasswordRule implements Rule
{
    public function passes($attribute, $value)
    {
        return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,}$/', $value);
    }

    public function message()
    {
        return 'Password must be at least 12 characters with mixed case, numbers, and symbols.';
    }
}

File Upload Security

class FileUploadRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'file' => [
                'required',
                'file',
                'max:2048', // 2MB
                'mimes:jpg,jpeg,png,pdf',
                new NoMaliciousContent,
            ],
        ];
    }
}

class NoMaliciousContent implements Rule
{
    public function passes($attribute, $value)
    {
        // Check file signatures
        $allowedSignatures = [
            'jpg' => 'ffd8ffe0',
            'png' => '89504e47',
            'pdf' => '25504446',
        ];

        $fileSignature = bin2hex(fread(fopen($value->path(), 'r'), 4));

        return in_array($fileSignature, $allowedSignatures);
    }
}

Security Headers Middleware

class SecurityHeadersMiddleware
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('X-XSS-Protection', '1; mode=block');
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');

        // CSP Header
        $csp = "default-src 'self'; script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src 'self' fonts.gstatic.com; img-src 'self' data: https:;";
        $response->headers->set('Content-Security-Policy', $csp);

        return $response;
    }
}

Logging & Monitoring

Security Event Logging

class SecurityLogger
{
    public static function logFailedLogin($email, $ip)
    {
        Log::channel('security')->warning('Failed login attempt', [
            'email' => $email,
            'ip' => $ip,
            'user_agent' => request()->userAgent(),
            'timestamp' => now(),
        ]);
    }

    public static function logSuspiciousActivity($user, $activity)
    {
        Log::channel('security')->alert('Suspicious activity detected', [
            'user_id' => $user->id,
            'activity' => $activity,
            'ip' => request()->ip(),
            'timestamp' => now(),
        ]);

        // Trigger alerts
        event(new SuspiciousActivityDetected($user, $activity));
    }
}

Key Takeaways

  1. Defense in Depth - Layer your security controls
  2. Validate Everything - Never trust user input
  3. Principle of Least Privilege - Give minimum required access
  4. Monitor & Log - You can’t secure what you can’t see
  5. Regular Updates - Keep dependencies current

Tools I Recommend

Security isn’t a feature you add later—it’s a mindset you adopt from day one. These patterns have helped me build and maintain secure Laravel applications in production environments.

What security practices have you found most effective? Let me know on LinkedIn.

🔗 Read more