Comprehensive security guide for the Recipe Share API backend with authentication, authorization, and security best practices.
- Overview
- Authentication
- Authorization
- Input Validation
- Rate Limiting
- Security Headers
- Data Protection
- File Upload Security
- API Security
- Monitoring & Logging
- Security Best Practices
- Incident Response
The Recipe Share API implements multi-layered security controls to protect user data, prevent unauthorized access, and maintain system integrity.
- ✅ 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 | 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 |
# 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<?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,
],
];<?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);
}
}
}<?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));
}
}<?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
}
}<?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'));
}
}<?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);
}
}<?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);
}
}<?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);
}
}<?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())
];
});
}
}<?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
};
}
}<?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;
}
}<?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'));
}
}<?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;
}
}<?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);
}
}
}<?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)
];
}
}<?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
}
}<?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'
]);
}
}
}
}## 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! 🔒