Skip to content

Security: Zsomi/RecipeShare-Web

Security

docs/SECURITY.md

Security Documentation

Comprehensive security guide for the Recipe Share API backend with authentication, authorization, and security best practices.

📋 Table of Contents


🎯 Overview

The Recipe Share API implements multi-layered security controls to protect user data, prevent unauthorized access, and maintain system integrity.

Security Architecture

  • Authentication: Laravel Passport OAuth2 with JWT tokens
  • Authorization: Spatie Laravel Permission with role-based access
  • Input Validation: Custom form requests with strict validation rules
  • Rate Limiting: Configurable throttling with Redis backend
  • Data Protection: Encryption, hashing, and secure storage
  • File Security: Upload validation, virus scanning, secure storage
  • Monitoring: Activity logging, security event tracking
  • HTTPS Enforcement: SSL/TLS encryption for all communications

Threat Model

Threat Mitigation Priority
Unauthorized Access OAuth2 + RBAC Critical
Data Breaches Encryption + Access Controls Critical
Injection Attacks Input validation + Prepared statements High
DDoS/Abuse Rate limiting + WAF High
File Upload Attacks Validation + Sandboxing Medium
Session Hijacking Secure cookies + CSRF protection Medium

🔐 Authentication

Laravel Passport OAuth2 Configuration

Installation & Setup

# Install Laravel Passport
composer require laravel/passport

# Run migrations
php artisan migrate

# Install Passport
php artisan passport:install

# Generate encryption keys
php artisan passport:keys

Passport Configuration: config/passport.php

<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Passport Guard
    |--------------------------------------------------------------------------
    */
    'guard' => 'web',

    /*
    |--------------------------------------------------------------------------
    | Encryption Keys
    |--------------------------------------------------------------------------
    */
    'private_key' => env('PASSPORT_PRIVATE_KEY'),
    'public_key' => env('PASSPORT_PUBLIC_KEY'),

    /*
    |--------------------------------------------------------------------------
    | Token Expiration
    |--------------------------------------------------------------------------
    */
    'tokens' => [
        'access_token_lifetime' => env('ACCESS_TOKEN_LIFETIME', 900), // 15 minutes
        'refresh_token_lifetime' => env('REFRESH_TOKEN_LIFETIME', 1440), // 24 hours
        'personal_access_client_id' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_ID'),
        'personal_access_client_secret' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET'),
    ],

    /*
    |--------------------------------------------------------------------------
    | Token Pruning
    |--------------------------------------------------------------------------
    */
    'pruning' => [
        'revoked_tokens' => true,
        'expired_tokens' => true,
    ],
];

Auth Controller Implementation

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Requests\Auth\RegisterRequest;
use App\Models\User;
use App\Services\AuthService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use App\Helpers\ActivityHelper;

class AuthController extends Controller
{
    public function __construct(
        private AuthService $authService
    ) {}

    /**
     * Register new user
     */
    public function register(RegisterRequest $request): JsonResponse
    {
        try {
            $user = User::create([
                'name' => $request->name,
                'email' => $request->email,
                'password' => Hash::make($request->password),
                'email_verified_at' => null
            ]);

            // Assign default role
            $user->assignRole('user');

            // Generate tokens
            $tokens = $this->authService->generateTokens($user);

            // Log registration
            ActivityHelper::logSecurityEvent('user_registered', [
                'user_id' => $user->id,
                'email' => $user->email,
                'severity' => 'low'
            ]);

            return response()->json([
                'message' => 'User registered successfully',
                'user' => $user->makeHidden(['email_verified_at', 'created_at', 'updated_at']),
                'tokens' => $tokens
            ], 201);

        } catch (\Exception $e) {
            ActivityHelper::logSecurityEvent('registration_failed', [
                'email' => $request->email,
                'error' => $e->getMessage(),
                'severity' => 'medium'
            ]);

            return response()->json([
                'message' => 'Registration failed',
                'error' => 'Unable to create user account'
            ], 500);
        }
    }

    /**
     * Login user
     */
    public function login(LoginRequest $request): JsonResponse
    {
        $rateLimitKey = 'login-attempts:' . $request->ip();
        
        // Check rate limiting
        if (RateLimiter::tooManyAttempts($rateLimitKey, 5)) {
            $seconds = RateLimiter::availableIn($rateLimitKey);
            
            ActivityHelper::logSecurityEvent('login_rate_limit_exceeded', [
                'email' => $request->email,
                'ip_address' => $request->ip(),
                'severity' => 'high',
                'retry_after' => $seconds
            ]);
            
            return response()->json([
                'message' => 'Too many login attempts',
                'retry_after' => $seconds
            ], 429);
        }

        $user = User::where('email', $request->email)->first();

        if (!$user || !Hash::check($request->password, $user->password)) {
            RateLimiter::hit($rateLimitKey, 300); // 5 minutes penalty
            
            ActivityHelper::logSecurityEvent('login_failed', [
                'email' => $request->email,
                'reason' => 'invalid_credentials',
                'severity' => 'medium'
            ]);

            return response()->json([
                'message' => 'Invalid credentials'
            ], 401);
        }

        // Check if user is banned
        if ($user->hasRole('banned')) {
            ActivityHelper::logSecurityEvent('banned_user_login_attempt', [
                'user_id' => $user->id,
                'email' => $user->email,
                'severity' => 'high'
            ]);

            return response()->json([
                'message' => 'Account has been suspended'
            ], 403);
        }

        // Clear rate limiting on successful login
        RateLimiter::clear($rateLimitKey);

        // Generate tokens
        $tokens = $this->authService->generateTokens($user);

        // Update last login
        $user->update(['last_login_at' => now()]);

        // Log successful login
        ActivityHelper::logSecurityEvent('user_logged_in', [
            'user_id' => $user->id,
            'severity' => 'low'
        ]);

        return response()->json([
            'message' => 'Login successful',
            'user' => $user->makeHidden(['email_verified_at', 'created_at', 'updated_at']),
            'tokens' => $tokens
        ]);
    }

    /**
     * Refresh access token
     */
    public function refresh(Request $request): JsonResponse
    {
        $request->validate([
            'refresh_token' => 'required|string'
        ]);

        try {
            $tokens = $this->authService->refreshTokens($request->refresh_token);

            return response()->json([
                'message' => 'Token refreshed successfully',
                'tokens' => $tokens
            ]);

        } catch (\Exception $e) {
            ActivityHelper::logSecurityEvent('token_refresh_failed', [
                'error' => $e->getMessage(),
                'severity' => 'medium'
            ]);

            return response()->json([
                'message' => 'Token refresh failed'
            ], 401);
        }
    }

    /**
     * Logout user
     */
    public function logout(Request $request): JsonResponse
    {
        try {
            // Revoke all tokens for the user
            $request->user()->tokens->each(function ($token) {
                $token->delete();
            });

            ActivityHelper::logSecurityEvent('user_logged_out', [
                'user_id' => $request->user()->id,
                'severity' => 'low'
            ]);

            return response()->json([
                'message' => 'Logged out successfully'
            ]);

        } catch (\Exception $e) {
            return response()->json([
                'message' => 'Logout failed'
            ], 500);
        }
    }
}

Auth Service Implementation

<?php

namespace App\Services;

use App\Models\User;
use Laravel\Passport\TokenRepository;
use Laravel\Passport\RefreshTokenRepository;
use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Hash;

class AuthService
{
    public function __construct(
        private TokenRepository $tokenRepository,
        private RefreshTokenRepository $refreshTokenRepository
    ) {}

    /**
     * Generate access and refresh tokens
     */
    public function generateTokens(User $user): array
    {
        // Create personal access token
        $tokenResult = $user->createToken('Recipe Share API Token');
        $token = $tokenResult->token;
        
        // Set token expiration
        $token->expires_at = now()->addMinutes(config('passport.tokens.access_token_lifetime', 15));
        $token->save();

        // Generate refresh token
        $refreshToken = $this->generateRefreshToken($token);

        return [
            'access_token' => $tokenResult->accessToken,
            'refresh_token' => $refreshToken,
            'token_type' => 'Bearer',
            'expires_in' => config('passport.tokens.access_token_lifetime', 15) * 60
        ];
    }

    /**
     * Refresh access token using refresh token
     */
    public function refreshTokens(string $refreshToken): array
    {
        // Validate refresh token
        $tokenId = $this->validateRefreshToken($refreshToken);
        
        if (!$tokenId) {
            throw new \Exception('Invalid refresh token');
        }

        // Get the original token
        $originalToken = $this->tokenRepository->find($tokenId);
        
        if (!$originalToken || $originalToken->revoked) {
            throw new \Exception('Token not found or revoked');
        }

        // Revoke the old token
        $this->tokenRepository->revokeAccessToken($tokenId);

        // Create new token
        $user = User::find($originalToken->user_id);
        return $this->generateTokens($user);
    }

    /**
     * Validate and decode refresh token
     */
    private function validateRefreshToken(string $refreshToken): ?string
    {
        try {
            // Decode the refresh token (implement your own logic)
            $payload = json_decode(base64_decode($refreshToken), true);
            
            if (!$payload || !isset($payload['token_id'])) {
                return null;
            }

            return $payload['token_id'];
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * Generate refresh token
     */
    private function generateRefreshToken($token): string
    {
        $payload = [
            'token_id' => $token->id,
            'user_id' => $token->user_id,
            'expires_at' => now()->addMinutes(config('passport.tokens.refresh_token_lifetime', 1440))->timestamp
        ];

        return base64_encode(json_encode($payload));
    }
}

🛡️ Authorization

Role-Based Access Control (RBAC)

Roles and Permissions Setup

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

class RolesAndPermissionsSeeder extends Seeder
{
    public function run()
    {
        // Reset cached roles and permissions
        app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();

        // Create permissions
        $permissions = [
            // Recipe permissions
            'create recipes',
            'edit own recipes',
            'edit any recipes',
            'delete own recipes',
            'delete any recipes',
            'publish recipes',
            'moderate recipes',
            
            // Comment permissions
            'create comments',
            'edit own comments',
            'edit any comments',
            'delete own comments',
            'delete any comments',
            'moderate comments',
            
            // User permissions
            'view users',
            'edit own profile',
            'edit any profile',
            'delete users',
            'ban users',
            'assign roles',
            
            // Admin permissions
            'access admin panel',
            'view analytics',
            'manage settings',
            'view activity logs',
        ];

        foreach ($permissions as $permission) {
            Permission::create(['name' => $permission]);
        }

        // Create roles and assign permissions
        $user = Role::create(['name' => 'user']);
        $user->givePermissionTo([
            'create recipes',
            'edit own recipes',
            'delete own recipes',
            'create comments',
            'edit own comments',
            'delete own comments',
            'edit own profile'
        ]);

        $moderator = Role::create(['name' => 'moderator']);
        $moderator->givePermissionTo([
            'create recipes',
            'edit own recipes',
            'edit any recipes',
            'delete own recipes',
            'moderate recipes',
            'create comments',
            'edit own comments',
            'edit any comments',
            'delete own comments',
            'moderate comments',
            'view users',
            'edit own profile'
        ]);

        $admin = Role::create(['name' => 'admin']);
        $admin->givePermissionTo(Permission::all());

        $banned = Role::create(['name' => 'banned']);
        // Banned users have no permissions
    }
}

Authorization Policies

<?php

namespace App\Policies;

use App\Models\Recipe;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class RecipePolicy
{
    use HandlesAuthorization;

    /**
     * Determine if user can view any recipes
     */
    public function viewAny(User $user): bool
    {
        return !$user->hasRole('banned');
    }

    /**
     * Determine if user can view the recipe
     */
    public function view(?User $user, Recipe $recipe): bool
    {
        // Public recipes can be viewed by anyone
        if ($recipe->is_public) {
            return true;
        }

        // Private recipes can only be viewed by owner or admins
        return $user && (
            $user->id === $recipe->user_id ||
            $user->hasPermissionTo('edit any recipes')
        );
    }

    /**
     * Determine if user can create recipes
     */
    public function create(User $user): bool
    {
        return $user->hasPermissionTo('create recipes') && !$user->hasRole('banned');
    }

    /**
     * Determine if user can update the recipe
     */
    public function update(User $user, Recipe $recipe): bool
    {
        if ($user->hasRole('banned')) {
            return false;
        }

        return $user->hasPermissionTo('edit any recipes') ||
            ($user->hasPermissionTo('edit own recipes') && $user->id === $recipe->user_id);
    }

    /**
     * Determine if user can delete the recipe
     */
    public function delete(User $user, Recipe $recipe): bool
    {
        if ($user->hasRole('banned')) {
            return false;
        }

        return $user->hasPermissionTo('delete any recipes') ||
            ($user->hasPermissionTo('delete own recipes') && $user->id === $recipe->user_id);
    }

    /**
     * Determine if user can publish the recipe
     */
    public function publish(User $user, Recipe $recipe): bool
    {
        return $user->hasPermissionTo('publish recipes') && 
            ($user->id === $recipe->user_id || $user->hasPermissionTo('edit any recipes'));
    }
}

Middleware for Authorization

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Helpers\ActivityHelper;

class CheckPermission
{
    /**
     * Handle an incoming request
     */
    public function handle(Request $request, Closure $next, string $permission): JsonResponse
    {
        if (!auth()->check()) {
            return response()->json([
                'message' => 'Authentication required'
            ], 401);
        }

        $user = auth()->user();

        // Check if user is banned
        if ($user->hasRole('banned')) {
            ActivityHelper::logSecurityEvent('banned_user_access_attempt', [
                'user_id' => $user->id,
                'attempted_permission' => $permission,
                'route' => $request->route()->getName(),
                'severity' => 'high'
            ]);

            return response()->json([
                'message' => 'Account has been suspended'
            ], 403);
        }

        // Check permission
        if (!$user->hasPermissionTo($permission)) {
            ActivityHelper::logSecurityEvent('unauthorized_access_attempt', [
                'user_id' => $user->id,
                'required_permission' => $permission,
                'route' => $request->route()->getName(),
                'severity' => 'medium'
            ]);

            return response()->json([
                'message' => 'Insufficient permissions'
            ], 403);
        }

        return $next($request);
    }
}

✅ Input Validation

Custom Form Requests

<?php

namespace App\Http\Requests\Recipe;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class CreateRecipeRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request
     */
    public function authorize(): bool
    {
        return auth()->check() && auth()->user()->can('create recipes');
    }

    /**
     * Get the validation rules
     */
    public function rules(): array
    {
        return [
            'title' => [
                'required',
                'string',
                'min:3',
                'max:255',
                'regex:/^[a-zA-Z0-9\s\-\'\".,!?]+$/' // Allow alphanumeric, spaces, and common punctuation
            ],
            'description' => [
                'required',
                'string',
                'min:10',
                'max:2000'
            ],
            'ingredients' => [
                'required',
                'array',
                'min:1',
                'max:50'
            ],
            'ingredients.*' => [
                'required',
                'string',
                'max:255',
                'regex:/^[a-zA-Z0-9\s\-\'\".,!?()\/%]+$/'
            ],
            'steps' => [
                'required',
                'array',
                'min:1',
                'max:30'
            ],
            'steps.*' => [
                'required',
                'string',
                'max:1000'
            ],
            'cuisine_type' => [
                'required',
                'string',
                'in:italian,mexican,asian,american,french,indian,mediterranean,other'
            ],
            'difficulty' => [
                'required',
                'string',
                'in:easy,medium,hard'
            ],
            'preparation_time' => [
                'required',
                'integer',
                'min:1',
                'max:1440' // 24 hours in minutes
            ],
            'cooking_time' => [
                'required',
                'integer',
                'min:1',
                'max:1440'
            ],
            'servings' => [
                'required',
                'integer',
                'min:1',
                'max:100'
            ],
            'image' => [
                'sometimes',
                'image',
                'mimes:jpeg,png,jpg,webp',
                'max:5120', // 5MB
                'dimensions:min_width=300,min_height=300,max_width=4000,max_height=4000'
            ]
        ];
    }

    /**
     * Get custom validation messages
     */
    public function messages(): array
    {
        return [
            'title.regex' => 'The title contains invalid characters.',
            'ingredients.*.regex' => 'Ingredient contains invalid characters.',
            'cuisine_type.in' => 'Invalid cuisine type selected.',
            'difficulty.in' => 'Invalid difficulty level selected.',
            'image.dimensions' => 'Image must be between 300x300 and 4000x4000 pixels.',
        ];
    }

    /**
     * Handle a failed validation attempt
     */
    protected function failedValidation(Validator $validator)
    {
        // Log validation failure for security monitoring
        ActivityHelper::logSecurityEvent('validation_failed', [
            'route' => $this->route()->getName(),
            'errors' => $validator->errors()->toArray(),
            'input_size' => strlen(json_encode($this->all())),
            'severity' => 'low'
        ]);

        throw new HttpResponseException(
            response()->json([
                'message' => 'Validation failed',
                'errors' => $validator->errors()
            ], 422)
        );
    }

    /**
     * Prepare the data for validation
     */
    protected function prepareForValidation(): void
    {
        // Sanitize input data
        $this->merge([
            'title' => $this->sanitizeString($this->title),
            'description' => $this->sanitizeString($this->description),
            'ingredients' => $this->sanitizeArray($this->ingredients ?? []),
            'steps' => $this->sanitizeArray($this->steps ?? [])
        ]);
    }

    /**
     * Sanitize string input
     */
    private function sanitizeString(?string $value): ?string
    {
        if (!$value) return $value;
        
        // Remove null bytes and control characters
        $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value);
        
        // Trim whitespace
        return trim($value);
    }

    /**
     * Sanitize array input
     */
    private function sanitizeArray(array $values): array
    {
        return array_map(function ($value) {
            return is_string($value) ? $this->sanitizeString($value) : $value;
        }, $values);
    }
}

SQL Injection Prevention

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Recipe;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class SearchController extends Controller
{
    /**
     * Search recipes with SQL injection protection
     */
    public function search(Request $request): JsonResponse
    {
        $request->validate([
            'query' => 'required|string|max:255|regex:/^[a-zA-Z0-9\s\-\'\".,!?]+$/',
            'cuisine_type' => 'sometimes|string|in:italian,mexican,asian,american,french,indian,mediterranean,other',
            'difficulty' => 'sometimes|string|in:easy,medium,hard',
            'max_time' => 'sometimes|integer|min:1|max:1440',
            'rating_min' => 'sometimes|numeric|min:0|max:5'
        ]);

        // Use query builder with parameter binding (prevents SQL injection)
        $query = Recipe::query()
            ->where('is_public', true)
            ->where(function ($q) use ($request) {
                $searchTerm = '%' . $request->query . '%';
                $q->where('title', 'LIKE', $searchTerm)
                  ->orWhere('description', 'LIKE', $searchTerm);
            });

        // Apply filters with parameter binding
        if ($request->has('cuisine_type')) {
            $query->where('cuisine_type', '=', $request->cuisine_type);
        }

        if ($request->has('difficulty')) {
            $query->where('difficulty', '=', $request->difficulty);
        }

        if ($request->has('max_time')) {
            $query->where('preparation_time', '<=', $request->max_time);
        }

        if ($request->has('rating_min')) {
            $query->where('average_rating', '>=', $request->rating_min);
        }

        $recipes = $query->with(['user:id,name'])
            ->select(['id', 'title', 'description', 'cuisine_type', 'difficulty', 'preparation_time', 'average_rating', 'image_path', 'user_id'])
            ->paginate(20);

        return response()->json($recipes);
    }
}

🚦 Rate Limiting

Rate Limiting Configuration

<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;

class RouteServiceProvider extends ServiceProvider
{
    /**
     * Configure the rate limiters for the application
     */
    protected function configureRateLimiting(): void
    {
        // General API rate limiting
        RateLimiter::for('api', function (Request $request) {
            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
        });

        // Authentication rate limiting
        RateLimiter::for('auth', function (Request $request) {
            return [
                Limit::perMinute(5)->by($request->ip()),
                Limit::perHour(20)->by($request->ip())
            ];
        });

        // Upload rate limiting
        RateLimiter::for('uploads', function (Request $request) {
            return [
                Limit::perMinute(3)->by($request->user()?->id ?: $request->ip()),
                Limit::perHour(50)->by($request->user()?->id ?: $request->ip())
            ];
        });

        // Search rate limiting
        RateLimiter::for('search', function (Request $request) {
            return [
                Limit::perMinute(30)->by($request->user()?->id ?: $request->ip()),
                Limit::perHour(500)->by($request->user()?->id ?: $request->ip())
            ];
        });

        // Admin operations rate limiting
        RateLimiter::for('admin', function (Request $request) {
            return Limit::perMinute(10)->by($request->user()?->id ?: $request->ip());
        });

        // Password reset rate limiting
        RateLimiter::for('password-reset', function (Request $request) {
            return [
                Limit::perMinute(1)->by($request->ip()),
                Limit::perHour(5)->by($request->ip())
            ];
        });
    }
}

Rate Limiting Middleware

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\JsonResponse;
use App\Helpers\ActivityHelper;

class CustomRateLimit
{
    /**
     * Handle an incoming request
     */
    public function handle(Request $request, Closure $next, string $limiter): JsonResponse
    {
        $key = $this->resolveRequestSignature($request, $limiter);
        
        if (RateLimiter::tooManyAttempts($key, $this->getMaxAttempts($limiter))) {
            $retryAfter = RateLimiter::availableIn($key);
            
            // Log rate limit violation
            ActivityHelper::logSecurityEvent('rate_limit_exceeded', [
                'limiter' => $limiter,
                'key' => $key,
                'user_id' => auth()->id(),
                'retry_after' => $retryAfter,
                'severity' => 'medium'
            ]);
            
            return response()->json([
                'message' => 'Too many requests',
                'retry_after' => $retryAfter
            ], 429);
        }
        
        RateLimiter::hit($key);
        
        $response = $next($request);
        
        // Add rate limit headers
        $response->headers->add([
            'X-RateLimit-Limit' => $this->getMaxAttempts($limiter),
            'X-RateLimit-Remaining' => RateLimiter::remaining($key, $this->getMaxAttempts($limiter)),
            'X-RateLimit-Reset' => now()->addSeconds($this->getDecaySeconds($limiter))->timestamp
        ]);
        
        return $response;
    }

    /**
     * Resolve request signature
     */
    protected function resolveRequestSignature(Request $request, string $limiter): string
    {
        if (auth()->check()) {
            return "{$limiter}:" . auth()->id();
        }
        
        return "{$limiter}:" . $request->ip();
    }

    /**
     * Get max attempts for limiter
     */
    protected function getMaxAttempts(string $limiter): int
    {
        return match($limiter) {
            'auth' => 5,
            'uploads' => 3,
            'search' => 30,
            'admin' => 10,
            'password-reset' => 1,
            default => 60
        };
    }

    /**
     * Get decay seconds for limiter
     */
    protected function getDecaySeconds(string $limiter): int
    {
        return match($limiter) {
            'password-reset' => 60,
            default => 60
        };
    }
}

🔒 Security Headers

Security Headers Middleware

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SecurityHeaders
{
    /**
     * Handle an incoming request
     */
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        // Security headers
        $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=()');
        
        // Content Security Policy
        $csp = "default-src 'self'; " .
               "script-src 'self' 'unsafe-inline'; " .
               "style-src 'self' 'unsafe-inline'; " .
               "img-src 'self' data: https:; " .
               "font-src 'self'; " .
               "connect-src 'self'; " .
               "frame-ancestors 'none';";
        
        $response->headers->set('Content-Security-Policy', $csp);

        // HSTS (if HTTPS)
        if ($request->isSecure()) {
            $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
        }

        return $response;
    }
}

🛡️ Data Protection

Encryption Service

<?php

namespace App\Services;

use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;

class EncryptionService
{
    /**
     * Encrypt sensitive data
     */
    public function encryptSensitiveData(string $data): string
    {
        return Crypt::encrypt($data);
    }

    /**
     * Decrypt sensitive data
     */
    public function decryptSensitiveData(string $encryptedData): string
    {
        try {
            return Crypt::decrypt($encryptedData);
        } catch (\Exception $e) {
            throw new \Exception('Failed to decrypt data');
        }
    }

    /**
     * Hash password securely
     */
    public function hashPassword(string $password): string
    {
        return Hash::make($password, [
            'rounds' => 12 // Increase for better security
        ]);
    }

    /**
     * Verify password hash
     */
    public function verifyPassword(string $password, string $hash): bool
    {
        return Hash::check($password, $hash);
    }

    /**
     * Generate secure random token
     */
    public function generateSecureToken(int $length = 32): string
    {
        return bin2hex(random_bytes($length));
    }

    /**
     * Hash sensitive identifier (for logging)
     */
    public function hashIdentifier(string $identifier): string
    {
        return hash('sha256', $identifier . config('app.key'));
    }
}

Database Encryption

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;

class UserProfile extends Model
{
    protected $fillable = [
        'user_id', 'phone', 'address', 'bio'
    ];

    protected $casts = [
        'phone' => 'encrypted',
        'address' => 'encrypted'
    ];

    /**
     * Encrypt phone number before saving
     */
    public function setPhoneAttribute($value)
    {
        $this->attributes['phone'] = $value ? Crypt::encrypt($value) : null;
    }

    /**
     * Decrypt phone number when retrieving
     */
    public function getPhoneAttribute($value)
    {
        try {
            return $value ? Crypt::decrypt($value) : null;
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * Get hashed phone for logging (privacy-safe)
     */
    public function getHashedPhoneAttribute(): ?string
    {
        $phone = $this->phone;
        return $phone ? hash('sha256', $phone) : null;
    }
}

📁 File Upload Security

Secure File Upload Controller

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Services\FileUploadService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class FileUploadController extends Controller
{
    public function __construct(
        private FileUploadService $fileUploadService
    ) {}

    /**
     * Upload recipe image with security checks
     */
    public function uploadRecipeImage(Request $request): JsonResponse
    {
        $request->validate([
            'image' => [
                'required',
                'file',
                'image',
                'mimes:jpeg,png,jpg,webp',
                'max:5120', // 5MB
                'dimensions:min_width=300,min_height=300,max_width=4000,max_height=4000'
            ]
        ]);

        try {
            $file = $request->file('image');
            
            // Security checks
            $securityCheck = $this->fileUploadService->performSecurityChecks($file);
            
            if (!$securityCheck['safe']) {
                return response()->json([
                    'message' => 'File failed security checks',
                    'reason' => $securityCheck['reason']
                ], 400);
            }

            // Process and store the file
            $result = $this->fileUploadService->uploadRecipeImage($file, auth()->id());

            return response()->json([
                'message' => 'Image uploaded successfully',
                'image_path' => $result['path'],
                'thumbnail_path' => $result['thumbnail_path']
            ]);

        } catch (\Exception $e) {
            return response()->json([
                'message' => 'Upload failed',
                'error' => 'Unable to process file'
            ], 500);
        }
    }
}

File Upload Security Service

<?php

namespace App\Services;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use App\Helpers\ActivityHelper;

class FileUploadService
{
    private array $allowedMimeTypes = [
        'image/jpeg',
        'image/png', 
        'image/jpg',
        'image/webp'
    ];

    private array $allowedExtensions = ['jpg', 'jpeg', 'png', 'webp'];

    /**
     * Perform comprehensive security checks on uploaded file
     */
    public function performSecurityChecks(UploadedFile $file): array
    {
        // Check file size
        if ($file->getSize() > 5 * 1024 * 1024) { // 5MB
            return ['safe' => false, 'reason' => 'File too large'];
        }

        // Check MIME type
        if (!in_array($file->getMimeType(), $this->allowedMimeTypes)) {
            return ['safe' => false, 'reason' => 'Invalid MIME type'];
        }

        // Check file extension
        $extension = strtolower($file->getClientOriginalExtension());
        if (!in_array($extension, $this->allowedExtensions)) {
            return ['safe' => false, 'reason' => 'Invalid file extension'];
        }

        // Check if file is actually an image
        try {
            $imageInfo = getimagesize($file->getRealPath());
            if (!$imageInfo) {
                return ['safe' => false, 'reason' => 'Not a valid image file'];
            }
        } catch (\Exception $e) {
            return ['safe' => false, 'reason' => 'Cannot read image file'];
        }

        // Check for malicious content in file headers
        $content = file_get_contents($file->getRealPath(), false, null, 0, 1024);
        if ($this->containsMaliciousContent($content)) {
            return ['safe' => false, 'reason' => 'Malicious content detected'];
        }

        // Virus scan (if available)
        if ($this->isVirusScanAvailable()) {
            $scanResult = $this->scanForViruses($file->getRealPath());
            if (!$scanResult['clean']) {
                ActivityHelper::logSecurityEvent('virus_detected_in_upload', [
                    'file_name' => $file->getClientOriginalName(),
                    'virus_info' => $scanResult['details'],
                    'severity' => 'critical'
                ]);
                
                return ['safe' => false, 'reason' => 'Virus detected'];
            }
        }

        return ['safe' => true];
    }

    /**
     * Upload and process recipe image
     */
    public function uploadRecipeImage(UploadedFile $file, int $userId): array
    {
        // Generate secure filename
        $filename = $this->generateSecureFilename($file);
        $path = "recipes/{$userId}/{$filename}";

        // Process image (resize, optimize, remove EXIF)
        $processedImage = $this->processImage($file);

        // Store the processed image
        Storage::disk('public')->put($path, $processedImage);

        // Generate thumbnail
        $thumbnailPath = "recipes/{$userId}/thumbnails/{$filename}";
        $thumbnail = $this->generateThumbnail($file);
        Storage::disk('public')->put($thumbnailPath, $thumbnail);

        // Log upload activity
        ActivityHelper::logUserAction('image_uploaded', [
            'file_name' => $file->getClientOriginalName(),
            'file_size' => $file->getSize(),
            'processed_path' => $path
        ]);

        return [
            'path' => $path,
            'thumbnail_path' => $thumbnailPath
        ];
    }

    /**
     * Generate secure filename
     */
    private function generateSecureFilename(UploadedFile $file): string
    {
        $extension = strtolower($file->getClientOriginalExtension());
        return hash('sha256', uniqid() . time()) . '.' . $extension;
    }

    /**
     * Process image for security and optimization
     */
    private function processImage(UploadedFile $file): string
    {
        $manager = new ImageManager(['driver' => 'gd']);
        $image = $manager->make($file->getRealPath());

        // Remove EXIF data for privacy
        $image->orientate();

        // Resize if too large
        if ($image->width() > 1920 || $image->height() > 1920) {
            $image->resize(1920, 1920, function ($constraint) {
                $constraint->aspectRatio();
                $constraint->upsize();
            });
        }

        // Optimize and convert to JPEG for consistency
        return $image->encode('jpg', 85)->getEncoded();
    }

    /**
     * Generate thumbnail
     */
    private function generateThumbnail(UploadedFile $file): string
    {
        $manager = new ImageManager(['driver' => 'gd']);
        $image = $manager->make($file->getRealPath());

        $image->fit(300, 300);
        return $image->encode('jpg', 80)->getEncoded();
    }

    /**
     * Check for malicious content patterns
     */
    private function containsMaliciousContent(string $content): bool
    {
        $maliciousPatterns = [
            '<?php',
            '<%',
            '<script',
            'javascript:',
            'eval(',
            'exec(',
            'system(',
            'shell_exec('
        ];

        foreach ($maliciousPatterns as $pattern) {
            if (stripos($content, $pattern) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if virus scanning is available
     */
    private function isVirusScanAvailable(): bool
    {
        return function_exists('exec') && 
               !in_array('exec', explode(',', ini_get('disable_functions'))) &&
               exec('which clamdscan') !== '';
    }

    /**
     * Scan file for viruses using ClamAV
     */
    private function scanForViruses(string $filePath): array
    {
        if (!$this->isVirusScanAvailable()) {
            return ['clean' => true];
        }

        $output = [];
        $returnCode = 0;
        
        exec("clamdscan --no-summary {$filePath} 2>&1", $output, $returnCode);
        
        return [
            'clean' => $returnCode === 0,
            'details' => implode("\n", $output)
        ];
    }
}

🔍 Monitoring & Logging

Security Event Monitoring

<?php

namespace App\Services;

use App\Models\SecurityEvent;
use App\Helpers\ActivityHelper;
use Illuminate\Support\Facades\Mail;
use App\Mail\SecurityAlertMail;

class SecurityMonitoringService
{
    private array $criticalEvents = [
        'multiple_failed_logins',
        'privilege_escalation_attempt',
        'virus_detected_in_upload',
        'sql_injection_attempt',
        'admin_account_compromise'
    ];

    /**
     * Monitor and respond to security events
     */
    public function handleSecurityEvent(string $eventType, array $data): void
    {
        // Log the event
        $securityEvent = SecurityEvent::create([
            'event_type' => $eventType,
            'severity' => $data['severity'] ?? 'medium',
            'data' => $data,
            'ip_address' => request()->ip(),
            'user_agent' => request()->userAgent(),
            'user_id' => auth()->id()
        ]);

        // Check for critical events
        if (in_array($eventType, $this->criticalEvents)) {
            $this->handleCriticalEvent($securityEvent);
        }

        // Check for patterns
        $this->analyzeEventPatterns($eventType, $data);
    }

    /**
     * Handle critical security events
     */
    private function handleCriticalEvent(SecurityEvent $event): void
    {
        // Send immediate alert to security team
        Mail::to(config('security.alert_email'))
            ->send(new SecurityAlertMail($event));

        // Auto-ban if necessary
        if ($this->shouldAutoBan($event)) {
            $this->performAutoBan($event);
        }
    }

    /**
     * Analyze event patterns for anomalies
     */
    private function analyzeEventPatterns(string $eventType, array $data): void
    {
        $ipAddress = request()->ip();
        
        // Check for brute force attacks
        if ($eventType === 'login_failed') {
            $recentFailures = SecurityEvent::where('event_type', 'login_failed')
                ->where('ip_address', $ipAddress)
                ->where('created_at', '>', now()->subMinutes(15))
                ->count();

            if ($recentFailures >= 10) {
                $this->handleSecurityEvent('brute_force_detected', [
                    'ip_address' => $ipAddress,
                    'failure_count' => $recentFailures,
                    'severity' => 'high'
                ]);
            }
        }

        // Check for suspicious user behavior
        if (auth()->check()) {
            $this->analyzeUserBehavior(auth()->id(), $eventType);
        }
    }

    /**
     * Analyze user behavior patterns
     */
    private function analyzeUserBehavior(int $userId, string $eventType): void
    {
        $suspiciousEvents = SecurityEvent::where('user_id', $userId)
            ->whereIn('event_type', [
                'unauthorized_access_attempt',
                'privilege_escalation_attempt',
                'suspicious_file_upload'
            ])
            ->where('created_at', '>', now()->subHours(24))
            ->count();

        if ($suspiciousEvents >= 5) {
            $this->handleSecurityEvent('suspicious_user_behavior', [
                'user_id' => $userId,
                'suspicious_event_count' => $suspiciousEvents,
                'severity' => 'high'
            ]);
        }
    }

    /**
     * Determine if auto-ban should be triggered
     */
    private function shouldAutoBan(SecurityEvent $event): bool
    {
        return in_array($event->event_type, [
            'virus_detected_in_upload',
            'sql_injection_attempt',
            'brute_force_detected'
        ]) && $event->severity === 'critical';
    }

    /**
     * Perform automatic ban
     */
    private function performAutoBan(SecurityEvent $event): void
    {
        if ($event->user_id) {
            $user = User::find($event->user_id);
            if ($user && !$user->hasRole('admin')) {
                $user->assignRole('banned');
                
                ActivityHelper::logSecurityEvent('user_auto_banned', [
                    'user_id' => $user->id,
                    'reason' => $event->event_type,
                    'auto_ban' => true,
                    'severity' => 'critical'
                ]);
            }
        }

        // Also consider IP-based blocking
        $this->addToBlacklist($event->ip_address, $event->event_type);
    }

    /**
     * Add IP to blacklist
     */
    private function addToBlacklist(string $ipAddress, string $reason): void
    {
        // Implement IP blacklisting logic
        // This could involve updating firewall rules, 
        // adding to Redis blacklist, or updating database
    }
}

🚨 Incident Response

Incident Response Plan

<?php

namespace App\Services;

use App\Models\SecurityIncident;
use Illuminate\Support\Facades\Mail;
use App\Mail\IncidentResponseMail;

class IncidentResponseService
{
    /**
     * Handle security incident
     */
    public function handleIncident(string $type, array $details): SecurityIncident
    {
        // Create incident record
        $incident = SecurityIncident::create([
            'type' => $type,
            'severity' => $this->determineSeverity($type, $details),
            'details' => $details,
            'status' => 'open',
            'detected_at' => now(),
            'reporter_id' => auth()->id()
        ]);

        // Immediate response actions
        $this->performImmediateResponse($incident);

        // Notify security team
        $this->notifySecurityTeam($incident);

        return $incident;
    }

    /**
     * Determine incident severity
     */
    private function determineSeverity(string $type, array $details): string
    {
        $criticalTypes = [
            'data_breach',
            'admin_compromise',
            'system_compromise',
            'massive_data_loss'
        ];

        if (in_array($type, $criticalTypes)) {
            return 'critical';
        }

        $highTypes = [
            'privilege_escalation',
            'unauthorized_data_access',
            'service_disruption'
        ];

        if (in_array($type, $highTypes)) {
            return 'high';
        }

        return 'medium';
    }

    /**
     * Perform immediate response actions
     */
    private function performImmediateResponse(SecurityIncident $incident): void
    {
        switch ($incident->type) {
            case 'data_breach':
                $this->handleDataBreach($incident);
                break;
                
            case 'admin_compromise':
                $this->handleAdminCompromise($incident);
                break;
                
            case 'system_compromise':
                $this->handleSystemCompromise($incident);
                break;
                
            default:
                $this->handleGenericIncident($incident);
        }
    }

    /**
     * Handle data breach incident
     */
    private function handleDataBreach(SecurityIncident $incident): void
    {
        // 1. Isolate affected systems
        // 2. Preserve evidence
        // 3. Assess scope of breach
        // 4. Prepare user notifications
        
        ActivityHelper::logSecurityEvent('data_breach_response_initiated', [
            'incident_id' => $incident->id,
            'severity' => 'critical'
        ]);
    }

    /**
     * Handle admin account compromise
     */
    private function handleAdminCompromise(SecurityIncident $incident): void
    {
        // 1. Disable compromised admin accounts
        // 2. Force password reset for all admin accounts
        // 3. Review admin activity logs
        // 4. Check for unauthorized changes
        
        // Disable potentially compromised admin accounts
        if (isset($incident->details['user_id'])) {
            $user = User::find($incident->details['user_id']);
            if ($user && $user->hasRole('admin')) {
                $user->tokens()->delete(); // Revoke all tokens
                
                ActivityHelper::logSecurityEvent('admin_account_disabled', [
                    'user_id' => $user->id,
                    'reason' => 'compromise_suspected',
                    'incident_id' => $incident->id,
                    'severity' => 'critical'
                ]);
            }
        }
    }
}

Security Checklist

## Daily Security Checklist

- [ ] Review security event logs
- [ ] Check for failed login attempts
- [ ] Monitor system resource usage
- [ ] Verify backup integrity
- [ ] Update security signatures

## Weekly Security Checklist

- [ ] Review user access permissions
- [ ] Audit admin account activity
- [ ] Check for software updates
- [ ] Review rate limiting effectiveness
- [ ] Analyze security metrics

## Monthly Security Checklist

- [ ] Conduct security assessment
- [ ] Review and update security policies
- [ ] Test incident response procedures
- [ ] Security awareness training
- [ ] Penetration testing (quarterly)

Stay Secure! 🔒

There aren’t any published security advisories