nasirkhan/laravel-starter
Laravel 13 modular starter with separated frontend/backend. Includes auth & authorization, user/role management, admin backend, backups, log viewer, and custom artisan commands (install, update, module builder). Use as a base to build reusable modules.
Last Updated: February 3, 2026
This document outlines security best practices implemented in Laravel Starter and recommendations for maintaining secure applications.
Laravel Breeze Integration
Role-Based Access Control (Spatie Permission)
// Check permissions in controllers
$this->authorize('edit_posts');
// Check in Blade templates
[@can](https://github.com/can)('edit_posts')
<!-- Content -->
[@endcan](https://github.com/endcan)
// Check in routes
Route::middleware('can:edit_posts')->group(function () {
// Protected routes
});
Multi-Factor Authentication
Route::middleware(['auth', 'verified'])->group(function () {
// Protected routes
});
// Define gates in AuthServiceProvider
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
// Use in controllers
if (Gate::denies('update-post', $post)) {
abort(403);
}
SESSION_LIFETIME=120 # 2 hours
Form Request Validation All forms use dedicated Form Request classes:
// Example: app/Http/Requests/StorePostRequest.php
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
'status' => ['required', 'in:draft,published'],
];
}
Livewire Validation
use Livewire\Attributes\Validate;
#[Validate('required|email|unique:users')]
public string $email = '';
#[Validate('required|min:8')]
public string $password = '';
// ❌ BAD
$post->title = request('title');
// ✅ GOOD
$validated = $request->validated();
$post->title = $validated['title'];
'email' => ['required', 'email:strict', 'max:255'],
'url' => ['required', 'url', 'active_url'],
'ip' => ['required', 'ip'],
// Use strip_tags or HTMLPurifier
$clean = strip_tags($input, '<p><a><strong><em>');
'file' => ['required', 'file', 'mimes:pdf,doc,docx', 'max:10240'],
'image' => ['required', 'image', 'mimes:jpeg,png,jpg', 'max:2048'],
Automatic CSRF Protection
In Blade Templates:
<form method="POST" action="/profile">
[@csrf](https://github.com/csrf)
<!-- Form fields -->
</form>
In Livewire Components:
<!-- CSRF token automatically handled by Livewire -->
<form wire:submit="save">
<!-- Form fields -->
</form>
In JavaScript (Axios):
// CSRF token automatically included via meta tag
axios.post('/api/data', formData);
<form method="POST">
[@csrf](https://github.com/csrf)
<!-- Never forget this! -->
</form>
// Only exclude if absolutely necessary (e.g., webhooks)
protected $except = [
'webhook/stripe',
];
Automatic Escaping
{{ }}Content Security Policy Headers
// config/secure-headers.php
'csp' => [
'default-src' => ["'self'"],
'script-src' => ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net'],
'style-src' => ["'self'", "'unsafe-inline'"],
],
<!-- ✅ GOOD - Automatically escaped -->
{{ $user->bio }}
<!-- ⚠️ DANGEROUS - Only use for trusted content -->
{!! $trustedHtml !!}
use Mews\Purifier\Facades\Purifier;
$clean = Purifier::clean($userInput);
SECURE_HEADERS=true
CSP_ENABLED=true
Eloquent ORM
Query Builder
// ✅ SAFE - Uses parameter binding
DB::table('users')->where('email', $email)->get();
// ❌ DANGEROUS - Never do this
DB::select("SELECT * FROM users WHERE email = '$email'");
// ✅ GOOD
User::where('email', $request->email)->first();
// ✅ GOOD
DB::table('users')->where('email', $request->email)->get();
DB::select('SELECT * FROM users WHERE email = :email', [
'email' => $request->email
]);
$validated = $request->validate([
'email' => ['required', 'email'],
]);
User::where('email', $validated['email'])->first();
bcrypt Hashing
Password Requirements
// Minimum requirements enforced
Password::min(8)
->mixedCase()
->numbers()
->symbols()
->uncompromised()
Password Reset
'password' => [
'required',
'confirmed',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(),
],
// ❌ NEVER
Log::info('Password: ' . $request->password);
// ✅ GOOD
Log::info('User logged in', ['user_id' => $user->id]);
Route::post('/settings/critical', function () {
// Requires password confirmation
})->middleware('password.confirm');
Default Rate Limits
// routes/web.php
Route::middleware(['throttle:60,1'])->group(function () {
// 60 requests per minute
});
// API routes
Route::middleware(['throttle:api'])->group(function () {
// Configurable in RouteServiceProvider
});
Login Rate Limiting
// Breeze includes login throttling
// Max 5 attempts per minute per email
Route::post('/login')->middleware('throttle:5,1');
Route::post('/register')->middleware('throttle:3,1');
Route::post('/password/email')->middleware('throttle:3,10');
// bootstrap/app.php
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Log excessive requests
RateLimiter::for('strict', function (Request $request) {
if (RateLimiter::tooManyAttempts('strict:'.$request->ip(), 100)) {
Log::warning('Possible DDoS', ['ip' => $request->ip()]);
}
return Limit::perMinute(100)->by($request->ip());
});
Secure Session Configuration
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
Session Regeneration
# Development
SESSION_DRIVER=file
# Production
SESSION_DRIVER=database
# or
SESSION_DRIVER=redis
public function promoteToAdmin(User $user)
{
$user->assignRole('admin');
request()->session()->regenerate();
}
// Middleware to check last activity
if (time() - session('last_activity') > config('session.lifetime') * 60) {
Auth::logout();
session()->flush();
}
session(['last_activity' => time()]);
File Validation
'file' => [
'required',
'file',
'mimes:pdf,doc,docx,txt',
'max:10240', // 10MB
],
Secure Storage
'image' => [
'required',
'image',
'mimes:jpeg,png,jpg,gif',
'max:2048',
'dimensions:min_width=100,min_height=100,max_width=4000,max_height=4000',
],
// Use ClamAV or similar
if (! $this->isClean($file)) {
throw new \Exception('File failed security scan');
}
$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();
$file->storeAs('uploads', $filename, 'private');
public function download(string $filename)
{
$this->authorize('download-file', $filename);
return Storage::download('uploads/' . $filename);
}
Laravel Sanctum
API Rate Limiting
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
// API routes
});
// Generate tokens
$token = $user->createToken('mobile-app')->plainTextToken;
// Protect routes
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
Route::prefix('api/v1')->group(function () {
// Version 1 routes
});
Route::prefix('api/v2')->group(function () {
// Version 2 routes
});
return response()->json(['error' => 'Unauthorized'], 401);
return response()->json(['error' => 'Forbidden'], 403);
return response()->json(['error' => 'Not Found'], 404);
Environment Security
APP_DEBUG=false
APP_ENV=production
HTTPS Enforcement
// Force HTTPS in production
if (app()->environment('production')) {
URL::forceScheme('https');
}
# Set proper permissions
chmod 600 .env
# Add to .gitignore
echo ".env" >> .gitignore
APP_DEBUG=false
APP_ENV=production
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize
composer update --with-dependencies
npm audit fix
# nginx
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
Activity Logging (Spatie Activity Log)
activity()
->performedOn($post)
->causedBy($user)
->log('Post was updated');
Laravel Log Viewer
// Failed login attempts
Log::warning('Failed login attempt', [
'email' => $request->email,
'ip' => $request->ip(),
]);
// Suspicious activity
Log::alert('Multiple failed 2FA attempts', [
'user_id' => $user->id,
'ip' => $request->ip(),
]);
// Track unusual patterns
if ($user->login_count > 100 && $user->created_at->isToday()) {
Log::warning('Unusual activity detected', ['user_id' => $user->id]);
}
// Track all sensitive changes
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'updated_user_role',
'subject_type' => User::class,
'subject_id' => $user->id,
'old_values' => $user->getOriginal(),
'new_values' => $user->getChanges(),
]);
APP_DEBUG=falseAPP_ENV=productionAPP_KEYcomposer auditnpm auditIf you discover a security vulnerability in Laravel Starter, please email:
Do not create public GitHub issues for security vulnerabilities.
This document is maintained as part of the Laravel Starter project. Last updated: February 3, 2026
How can I help you explore Laravel packages today?