Laravel Security
Security

Securing Your Laravel Application

Mayur Dabhi
Mayur Dabhi
March 17, 2026
28 min read

Security isn't an afterthought—it's the foundation upon which trustworthy applications are built. Laravel provides a robust set of security features out of the box, but understanding how to properly implement and extend these protections is crucial for every developer. In this comprehensive guide, we'll explore everything you need to know to build Secure Laravel applications that can withstand modern threats.

Whether you're building a simple blog or a complex enterprise application handling sensitive data, the security principles and implementations covered here will help you protect your users, your data, and your reputation. We'll dive deep into authentication, authorization, input validation, encryption, and the many layers of defense that make Laravel one of the most secure PHP frameworks available.

What You'll Learn
  • Laravel's built-in security features and how they work
  • Protecting against CSRF, XSS, and SQL injection attacks
  • Implementing robust authentication and authorization
  • Securing APIs with Sanctum and rate limiting
  • Encryption, hashing, and secure data storage
  • Security headers and production hardening
  • Security audit checklist and best practices

Understanding Common Web Vulnerabilities

Before diving into Laravel's security features, let's understand the threats we're protecting against. The OWASP Top 10 represents the most critical web application security risks, and Laravel provides defenses against most of them out of the box.

Common Attack Vectors & Laravel Defenses ⚠️ Attack Vectors SQL Injection Malicious SQL in user inputs Cross-Site Scripting (XSS) Injected malicious scripts CSRF Attacks Forged authenticated requests Mass Assignment Unauthorized model attribute changes 🛡️ Laravel Defenses Eloquent ORM & Query Builder Parameterized queries by default Blade Auto-Escaping {{ }} escapes output automatically CSRF Tokens @csrf directive & VerifyCsrfToken $fillable & $guarded Explicit attribute whitelisting

Laravel provides multiple layers of defense against common web vulnerabilities

CSRF Protection: Preventing Cross-Site Request Forgery

Cross-Site Request Forgery (CSRF) attacks trick authenticated users into performing unwanted actions. Laravel protects against this by generating and validating unique tokens for each user session.

How CSRF Protection Works

🦹
Attacker Creates malicious form on evil-site.com targeting your-app.com
👤
Victim (Logged In) Visits evil-site.com while authenticated on your-app.com
🛡️
Laravel CSRF Middleware Rejects request - no valid CSRF token present!

Implementing CSRF Protection

Laravel's CSRF protection is enabled by default for all POST, PUT, PATCH, and DELETE requests. Here's how to implement it properly:

Blade Template - Form with CSRF
<form method="POST" action="/profile">
    @csrf
    
    <div class="form-group">
        <label for="name">Name</label>
        <input type="text" name="name" id="name" value="{{ old('name') }}">
    </div>
    
    <div class="form-group">
        <label for="email">Email</label>
        <input type="email" name="email" id="email" value="{{ old('email') }}">
    </div>
    
    <button type="submit">Update Profile</button>
</form>

<!-- For DELETE requests, use method spoofing -->
<form method="POST" action="/posts/{{ $post->id }}">
    @csrf
    @method('DELETE')
    <button type="submit">Delete Post</button>
</form>

For AJAX requests, you'll need to include the CSRF token in your headers:

JavaScript - AJAX with CSRF
// Set up CSRF token for all AJAX requests
const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');

// Using Fetch API
fetch('/api/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': token,
        'Accept': 'application/json'
    },
    body: JSON.stringify({ title: 'New Post', content: 'Content here' })
})
.then(response => response.json())
.then(data => console.log(data));

// Using Axios (auto-configured in Laravel)
axios.defaults.headers.common['X-CSRF-TOKEN'] = token;

axios.post('/api/posts', {
    title: 'New Post',
    content: 'Content here'
});
Pro Tip

Add the CSRF meta tag to your layout's <head> section: <meta name="csrf-token" content="{{ csrf_token() }}">. This makes the token easily accessible for JavaScript frameworks.

XSS Prevention: Escaping Output

Cross-Site Scripting (XSS) attacks inject malicious scripts into web pages viewed by other users. Laravel's Blade templating engine automatically escapes output, but you need to understand when and how to use it correctly.

Auto-Escaped Output

<!-- SAFE: Automatically escapes HTML entities -->
<p>Welcome, {{ $user->name }}</p>

<!-- Input: <script>alert('XSS')</script> -->
<!-- Output: &lt;script&gt;alert('XSS')&lt;/script&gt; -->

<!-- Safe in attributes too -->
<input value="{{ $userInput }}">

<!-- Use e() helper in PHP -->
<?php echo e($userInput); ?>

Dangerous - Raw Output

<!-- DANGEROUS: Never use with user input! -->
{!! $content !!}

<!-- Only use for trusted content like: -->
{!! $page->trusted_html !!}  <!-- Admin-created content -->
{!! $markdown->render() !!}  <!-- Sanitized markdown -->

<!-- NEVER do this: -->
{!! $request->input('comment') !!}  <!-- XSS vulnerability! -->
{!! $user->bio !!}  <!-- User-controlled = dangerous -->

Sanitized HTML

<!-- Use a sanitization library for user HTML -->
// Install: composer require mews/purifier

// config/purifier.php
'HTML.Allowed' => 'p,b,i,u,a[href],ul,ol,li,br',

// In your code:
use Mews\Purifier\Facades\Purifier;

$cleanHtml = Purifier::clean($userHtml);

// In Blade:
{!! Purifier::clean($post->content) !!}

// Or create a custom Blade directive
// AppServiceProvider:
Blade::directive('purify', function ($expression) {
    return "<?php echo \Purifier::clean($expression); ?>";
});

// Usage: @purify($post->content)

SQL Injection Prevention

SQL Injection occurs when attackers insert malicious SQL code through user inputs. Laravel's Eloquent ORM and Query Builder use PDO parameter binding, which automatically protects against SQL injection.

Safe Practices

Eloquent ORM, Query Builder with bindings, parameterized raw queries

Dangerous Practices

Raw queries with concatenated user input, unvalidated orderBy columns

Safe Database Queries
// ✅ SAFE: Eloquent ORM (always parameterized)
$users = User::where('email', $request->email)->get();
$user = User::find($id);

// ✅ SAFE: Query Builder with bindings
$users = DB::table('users')
    ->where('email', '=', $request->email)
    ->where('status', 'active')
    ->get();

// ✅ SAFE: Raw query with bindings
$users = DB::select(
    'SELECT * FROM users WHERE email = ? AND role = ?',
    [$request->email, 'admin']
);

// ✅ SAFE: Named bindings
$users = DB::select(
    'SELECT * FROM users WHERE email = :email',
    ['email' => $request->email]
);

// ✅ SAFE: whereRaw with bindings
$users = User::whereRaw('LOWER(email) = ?', [strtolower($email)])->get();
⚠️ Dangerous Patterns to Avoid
// ❌ DANGEROUS: String concatenation
$users = DB::select("SELECT * FROM users WHERE email = '$email'");

// ❌ DANGEROUS: Direct variable interpolation
$users = DB::select("SELECT * FROM users WHERE id = {$id}");

// ❌ DANGEROUS: Unvalidated column names in orderBy
$users = User::orderBy($request->sort_column)->get();

// ✅ SAFE: Whitelist allowed columns
$allowedColumns = ['name', 'email', 'created_at'];
$column = in_array($request->sort_column, $allowedColumns) 
    ? $request->sort_column 
    : 'created_at';
$users = User::orderBy($column)->get();

// ❌ DANGEROUS: whereRaw without bindings
User::whereRaw("email = '$email'")->get();

// ✅ SAFE: whereRaw with bindings
User::whereRaw('email = ?', [$email])->get();
Critical Warning

Never concatenate user input directly into SQL queries, even when using Laravel. The Query Builder and Eloquent are safe only when you use their parameter binding features correctly.

Authentication Best Practices

Laravel provides multiple authentication systems: the built-in authentication scaffolding, Laravel Breeze, Laravel Jetstream, and Laravel Fortify. Regardless of which you choose, these security practices apply.

Password Security

Secure Password Handling
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;

class RegisterController extends Controller
{
    public function store(Request $request)
    {
        // Strong password validation
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users'],
            'password' => [
                'required',
                'confirmed',
                Password::min(8)
                    ->letters()        // At least one letter
                    ->mixedCase()      // Upper and lowercase
                    ->numbers()        // At least one number
                    ->symbols()        // At least one symbol
                    ->uncompromised(), // Not in data breaches (HIBP API)
            ],
        ]);

        // Hash password using bcrypt (default) or argon2
        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);

        // Never store plain-text passwords!
        // ❌ 'password' => $validated['password']
        
        return redirect()->route('login')
            ->with('success', 'Account created successfully!');
    }
}

Session Security

Configure session security in config/session.php:

config/session.php
return [
    // Use database or redis for production
    'driver' => env('SESSION_DRIVER', 'database'),
    
    // Session lifetime in minutes
    'lifetime' => env('SESSION_LIFETIME', 120),
    
    // Expire session on browser close
    'expire_on_close' => false,
    
    // Encrypt session data
    'encrypt' => true,
    
    // Cookie settings
    'cookie' => env('SESSION_COOKIE', 'laravel_session'),
    
    // Restrict cookie to HTTPS only
    'secure' => env('SESSION_SECURE_COOKIE', true),
    
    // Prevent JavaScript access to cookie
    'http_only' => true,
    
    // SameSite cookie attribute
    'same_site' => 'lax', // or 'strict' for more protection
];

Multi-Factor Authentication

Implement 2FA using Laravel Fortify or a package like pragmarx/google2fa-laravel:

Two-Factor Authentication Setup
// Install: composer require pragmarx/google2fa-laravel

use PragmaRX\Google2FALaravel\Support\Authenticator;
use PragmaRX\Google2FA\Google2FA;

class TwoFactorController extends Controller
{
    public function enable(Request $request)
    {
        $google2fa = new Google2FA();
        
        // Generate secret key
        $secret = $google2fa->generateSecretKey();
        
        // Store encrypted in database
        $request->user()->update([
            'two_factor_secret' => encrypt($secret),
        ]);
        
        // Generate QR code URL
        $qrCodeUrl = $google2fa->getQRCodeUrl(
            config('app.name'),
            $request->user()->email,
            $secret
        );
        
        return view('2fa.enable', compact('qrCodeUrl', 'secret'));
    }
    
    public function verify(Request $request)
    {
        $request->validate(['code' => 'required|digits:6']);
        
        $google2fa = new Google2FA();
        $secret = decrypt($request->user()->two_factor_secret);
        
        $valid = $google2fa->verifyKey($secret, $request->code);
        
        if ($valid) {
            $request->user()->update(['two_factor_enabled' => true]);
            return redirect()->route('profile')
                ->with('success', '2FA enabled successfully!');
        }
        
        return back()->withErrors(['code' => 'Invalid verification code']);
    }
}

Authorization with Gates and Policies

Authorization determines what authenticated users can do. Laravel provides two primary mechanisms: Gates for simple checks and Policies for model-based authorization.

Laravel Authorization Flow 👤 User Request Gate / Policy authorize() $this->authorize() @can directive ✓ Allowed Continue to action ✗ Denied 403 Forbidden Response Error Page
Defining a Policy
// Create policy: php artisan make:policy PostPolicy --model=Post

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * Determine if user can view any posts.
     */
    public function viewAny(User $user): bool
    {
        return true; // Everyone can view posts list
    }
    
    /**
     * Determine if user can view the post.
     */
    public function view(User $user, Post $post): bool
    {
        // Published posts are public, drafts only for owner
        return $post->is_published || $user->id === $post->user_id;
    }
    
    /**
     * Determine if user can create posts.
     */
    public function create(User $user): bool
    {
        return $user->hasVerifiedEmail();
    }
    
    /**
     * Determine if user can update the post.
     */
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || $user->isAdmin();
    }
    
    /**
     * Determine if user can delete the post.
     */
    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || $user->isAdmin();
    }
    
    /**
     * Perform pre-authorization checks (admin bypass).
     */
    public function before(User $user, string $ability): ?bool
    {
        if ($user->isSuperAdmin()) {
            return true; // Super admins can do anything
        }
        
        return null; // Fall through to specific checks
    }
}
Using Authorization in Controllers
class PostController extends Controller
{
    public function __construct()
    {
        // Authorize all resource actions automatically
        $this->authorizeResource(Post::class, 'post');
    }
    
    public function show(Post $post)
    {
        // Authorization already handled by authorizeResource
        return view('posts.show', compact('post'));
    }
    
    public function update(Request $request, Post $post)
    {
        // Manual authorization (if not using authorizeResource)
        $this->authorize('update', $post);
        
        $post->update($request->validated());
        return redirect()->route('posts.show', $post);
    }
    
    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);
        
        $post->delete();
        return redirect()->route('posts.index')
            ->with('success', 'Post deleted successfully');
    }
}

// In Blade templates:
@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan

@cannot('delete', $post)
    <p>You cannot delete this post.</p>
@endcannot

@canany(['update', 'delete'], $post)
    <div class="admin-actions">...</div>
@endcanany

Input Validation & Sanitization

Never trust user input. Laravel's validation system provides a robust way to ensure data integrity and security.

Comprehensive Validation Example
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\File;
use Illuminate\Validation\Rules\Password;

class StoreUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // Or check permissions here
    }
    
    public function rules(): array
    {
        return [
            // String validation
            'name' => [
                'required',
                'string',
                'min:2',
                'max:100',
                'regex:/^[\pL\s\-]+$/u', // Letters, spaces, hyphens only
            ],
            
            // Email with DNS check
            'email' => [
                'required',
                'email:rfc,dns',
                'unique:users,email',
                'max:255',
            ],
            
            // Strong password
            'password' => [
                'required',
                'confirmed',
                Password::min(12)
                    ->letters()
                    ->mixedCase()
                    ->numbers()
                    ->symbols()
                    ->uncompromised(3), // Check against 3+ breaches
            ],
            
            // URL validation
            'website' => [
                'nullable',
                'url:http,https', // Only http/https
                'max:500',
            ],
            
            // Safe file upload
            'avatar' => [
                'nullable',
                File::image()
                    ->max(2 * 1024) // 2MB max
                    ->dimensions(
                        minWidth: 100,
                        minHeight: 100,
                        maxWidth: 2000,
                        maxHeight: 2000
                    ),
            ],
            
            // Integer in range
            'age' => ['nullable', 'integer', 'min:13', 'max:120'],
            
            // Enum/whitelist values
            'role' => ['required', 'in:user,moderator,admin'],
            
            // Array validation
            'tags' => ['nullable', 'array', 'max:10'],
            'tags.*' => ['string', 'max:50', 'alpha_dash'],
            
            // JSON data
            'settings' => ['nullable', 'json'],
            
            // Date validation
            'birth_date' => [
                'nullable',
                'date',
                'before:today',
                'after:1900-01-01',
            ],
        ];
    }
    
    public function messages(): array
    {
        return [
            'name.regex' => 'Name can only contain letters, spaces, and hyphens.',
            'email.dns' => 'Please provide a valid email with an active domain.',
            'password.uncompromised' => 'This password has been exposed in data breaches. Please choose a different one.',
        ];
    }
    
    /**
     * Sanitize input before validation
     */
    protected function prepareForValidation(): void
    {
        $this->merge([
            'name' => strip_tags($this->name),
            'email' => strtolower(trim($this->email)),
            'website' => $this->website ? trim($this->website) : null,
        ]);
    }
}

Rate Limiting & Throttling

Rate limiting protects your application from brute force attacks and API abuse. Laravel provides flexible rate limiting through the RateLimiter facade.

app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    // Default API rate limiter
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by(
            $request->user()?->id ?: $request->ip()
        );
    });
    
    // Strict login rate limiting (prevent brute force)
    RateLimiter::for('login', function (Request $request) {
        $email = strtolower($request->input('email'));
        
        return [
            // 5 attempts per email per minute
            Limit::perMinute(5)->by($email),
            // 3 attempts per IP per minute
            Limit::perMinute(3)->by($request->ip()),
        ];
    });
    
    // Password reset limiting
    RateLimiter::for('password-reset', function (Request $request) {
        return Limit::perHour(3)->by(
            $request->input('email') . '|' . $request->ip()
        );
    });
    
    // Expensive operations (exports, reports)
    RateLimiter::for('exports', function (Request $request) {
        return $request->user()->isPremium()
            ? Limit::perHour(100)
            : Limit::perHour(10);
    });
    
    // Global rate limit with custom response
    RateLimiter::for('global', function (Request $request) {
        return Limit::perMinute(1000)
            ->by($request->ip())
            ->response(function () {
                return response()->json([
                    'error' => 'Too many requests. Please slow down.',
                    'retry_after' => 60
                ], 429);
            });
    });
}
Applying Rate Limits to Routes
// routes/web.php
Route::post('/login', [LoginController::class, 'store'])
    ->middleware('throttle:login');

Route::post('/forgot-password', [PasswordResetController::class, 'store'])
    ->middleware('throttle:password-reset');

// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
    Route::apiResource('posts', PostController::class);
    
    // Custom rate limit for specific endpoint
    Route::get('/export', [ExportController::class, 'index'])
        ->middleware('throttle:exports');
});

Encryption & Secure Data Storage

Laravel uses AES-256-CBC encryption by default. Sensitive data should always be encrypted before storage.

Encryption in Practice
use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException;

class SecureDataService
{
    /**
     * Store sensitive data encrypted
     */
    public function storeSensitiveData(User $user, string $ssn): void
    {
        $user->update([
            'ssn_encrypted' => Crypt::encryptString($ssn),
        ]);
    }
    
    /**
     * Retrieve and decrypt sensitive data
     */
    public function getSensitiveData(User $user): ?string
    {
        if (!$user->ssn_encrypted) {
            return null;
        }
        
        try {
            return Crypt::decryptString($user->ssn_encrypted);
        } catch (DecryptException $e) {
            // Log the error, but don't expose details
            Log::error('Decryption failed for user', ['user_id' => $user->id]);
            return null;
        }
    }
}

// Using Encrypted Casts (Laravel 8+)
class User extends Model
{
    protected $casts = [
        'ssn' => 'encrypted',        // Auto encrypt/decrypt
        'api_key' => 'encrypted',
        'settings' => 'encrypted:array', // Encrypted JSON
    ];
}

// Now encryption is automatic:
$user->ssn = '123-45-6789';  // Encrypted on save
echo $user->ssn;              // Decrypted on access
Important: Protect Your APP_KEY

The APP_KEY in your .env file is used for all encryption. If it's compromised, all encrypted data can be decrypted. Never commit your .env file to version control, and rotate keys periodically with php artisan key:generate (requires re-encrypting existing data).

Security Headers

HTTP security headers add additional layers of protection. Configure these in middleware or your web server.

app/Http/Middleware/SecurityHeaders.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecurityHeaders
{
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);
        
        // Prevent XSS attacks
        $response->headers->set(
            'X-XSS-Protection', 
            '1; mode=block'
        );
        
        // Prevent clickjacking
        $response->headers->set(
            'X-Frame-Options', 
            'SAMEORIGIN'
        );
        
        // Prevent MIME type sniffing
        $response->headers->set(
            'X-Content-Type-Options', 
            'nosniff'
        );
        
        // Referrer policy
        $response->headers->set(
            'Referrer-Policy', 
            'strict-origin-when-cross-origin'
        );
        
        // Permissions policy
        $response->headers->set(
            'Permissions-Policy',
            'camera=(), microphone=(), geolocation=()'
        );
        
        // Content Security Policy (customize for your app)
        if (app()->environment('production')) {
            $response->headers->set(
                'Content-Security-Policy',
                "default-src 'self'; " .
                "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " .
                "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " .
                "font-src 'self' https://fonts.gstatic.com; " .
                "img-src 'self' data: https:; " .
                "connect-src 'self'; " .
                "frame-ancestors 'none';"
            );
        }
        
        // Strict Transport Security (HTTPS only)
        if ($request->secure()) {
            $response->headers->set(
                'Strict-Transport-Security',
                'max-age=31536000; includeSubDomains'
            );
        }
        
        return $response;
    }
}

Secure File Uploads

File uploads are a common attack vector. Always validate, sanitize, and store files securely.

Secure File Upload Implementation
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class FileUploadController extends Controller
{
    private array $allowedMimeTypes = [
        'image/jpeg',
        'image/png', 
        'image/gif',
        'image/webp',
        'application/pdf',
    ];
    
    private array $allowedExtensions = [
        'jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf'
    ];
    
    public function store(Request $request)
    {
        $request->validate([
            'file' => [
                'required',
                'file',
                'max:10240', // 10MB max
                'mimes:jpeg,png,gif,webp,pdf',
            ],
        ]);
        
        $file = $request->file('file');
        
        // Double-check MIME type (can be spoofed, but adds a layer)
        if (!in_array($file->getMimeType(), $this->allowedMimeTypes)) {
            abort(422, 'Invalid file type.');
        }
        
        // Verify extension
        $extension = strtolower($file->getClientOriginalExtension());
        if (!in_array($extension, $this->allowedExtensions)) {
            abort(422, 'Invalid file extension.');
        }
        
        // Generate random filename (prevent path traversal & overwrites)
        $filename = Str::uuid() . '.' . $extension;
        
        // Store outside web root (use 'local' disk, not 'public')
        $path = $file->storeAs(
            'uploads/' . date('Y/m'),
            $filename,
            'local' // Not publicly accessible
        );
        
        // If it needs to be public, use signed URLs
        // $url = Storage::temporaryUrl($path, now()->addHour());
        
        // Store metadata in database
        $upload = Upload::create([
            'user_id' => auth()->id(),
            'original_name' => $file->getClientOriginalName(),
            'path' => $path,
            'mime_type' => $file->getMimeType(),
            'size' => $file->getSize(),
        ]);
        
        return response()->json([
            'id' => $upload->id,
            'message' => 'File uploaded successfully',
        ]);
    }
    
    /**
     * Serve files securely through controller
     */
    public function show(Upload $upload)
    {
        // Authorization check
        $this->authorize('view', $upload);
        
        // Stream file from private storage
        return Storage::response($upload->path);
    }
}

Security Audit Checklist

Use this checklist to audit your Laravel application's security posture:

Environment Configuration APP_DEBUG=false in production, APP_ENV=production
HTTPS Enforced Force HTTPS in AppServiceProvider or web server config
CSRF Protection Active All forms include @csrf, VerifyCsrfToken middleware enabled
SQL Injection Prevention Using Eloquent/Query Builder, no raw string concatenation
XSS Prevention Using {{ }} for output, sanitizing user HTML
Password Security Using Hash::make(), strong password validation rules
Rate Limiting Login, API, and sensitive endpoints throttled
Authorization Checks Policies/Gates for all resource access
Mass Assignment Protection $fillable or $guarded defined on all models
Secure Session Config Secure cookies, HTTP-only, SameSite attribute set
Security Headers CSP, X-Frame-Options, HSTS configured
Dependencies Updated Run composer audit regularly, keep packages current

Production Hardening

.env (Production)
# Application
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourapp.com

# Security
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SANCTUM_STATEFUL_DOMAINS=yourapp.com

# Logging (don't log sensitive data)
LOG_CHANNEL=stack
LOG_LEVEL=warning

# Cache config and routes
# Run these during deployment:
# php artisan config:cache
# php artisan route:cache
# php artisan view:cache
Production Deployment Commands
#!/bin/bash
# deploy.sh

# Exit on error
set -e

# Put app in maintenance mode
php artisan down --secret="your-bypass-token"

# Pull latest code
git pull origin main

# Install dependencies (no dev)
composer install --no-dev --optimize-autoloader

# Run migrations
php artisan migrate --force

# Clear and cache config
php artisan config:clear
php artisan config:cache

# Cache routes
php artisan route:cache

# Cache views
php artisan view:cache

# Clear application cache
php artisan cache:clear

# Restart queue workers
php artisan queue:restart

# Security audit
composer audit

# Bring app back online
php artisan up

echo "Deployment complete!"

Key Takeaways

Security Principles

  • Defense in Depth: Multiple layers of security are better than relying on one
  • Least Privilege: Give users only the permissions they need
  • Never Trust User Input: Always validate and sanitize
  • Keep Updated: Regularly update Laravel and dependencies
  • Monitor & Log: Track suspicious activity and security events
  • Encrypt Sensitive Data: At rest and in transit

Security is an ongoing process, not a one-time setup. Regularly audit your application, stay informed about new vulnerabilities, and follow Laravel's security best practices. The framework provides excellent tools—your job is to use them correctly.

By implementing the security measures outlined in this guide, you'll have a solid foundation for building applications that protect both your users and your business. Remember: security is not about being paranoid; it's about being prepared.

Laravel Security Best Practices CSRF XSS Authentication Authorization
Mayur Dabhi

Mayur Dabhi

Full Stack Developer specializing in Laravel, React, and modern web technologies. Passionate about building secure, scalable applications.