Security Features - luckydeva03/barbershop_app GitHub Wiki
Panduan lengkap implementasi keamanan dalam barbershop management system untuk melindungi data dan mencegah serangan.
Sistem keamanan mencakup:
- Authentication & Authorization: Dual guard system (web/admin)
- Data Protection: Encryption, hashing, sanitization
- Input Validation: Form validation, CSRF protection
- Rate Limiting: API throttling, brute force protection
- Audit Logging: Activity tracking, security monitoring
- Database Security: Query protection, backup encryption
<?php
return [
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
'api' => [
'driver' => 'sanctum',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
'admins' => [
'provider' => 'admins',
'table' => 'admin_password_resets',
'expire' => 60,
'throttle' => 60,
],
],
'password_timeout' => 10800, // 3 hours
];
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\Hash;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
protected $fillable = [
'name',
'email',
'phone',
'google_id',
'avatar',
'email_verified_at',
'password',
'last_login_at',
'login_count',
'is_active',
];
protected $hidden = [
'password',
'remember_token',
'google_id',
];
protected $casts = [
'email_verified_at' => 'datetime',
'last_login_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
];
protected $dates = [
'deleted_at',
];
// Security Methods
public function recordLogin()
{
$this->update([
'last_login_at' => now(),
'login_count' => $this->login_count + 1,
]);
}
public function isActive()
{
return $this->is_active && !$this->deleted_at;
}
public function isSuspended()
{
return !$this->is_active || $this->deleted_at;
}
public function suspend($reason = null)
{
$this->update(['is_active' => false]);
// Log suspension
activity()
->performedOn($this)
->withProperties(['reason' => $reason])
->log('user_suspended');
}
public function activate()
{
$this->update(['is_active' => true]);
activity()
->performedOn($this)
->log('user_activated');
}
// Password Security
public function password(): Attribute
{
return Attribute::make(
set: fn ($value) => Hash::make($value),
);
}
public function changePassword($newPassword, $currentPassword = null)
{
if ($currentPassword && !Hash::check($currentPassword, $this->password)) {
throw new \Exception('Current password is incorrect');
}
$this->update(['password' => $newPassword]);
// Revoke all tokens for security
$this->tokens()->delete();
activity()
->performedOn($this)
->log('password_changed');
}
// Email Security
protected function email(): Attribute
{
return Attribute::make(
get: fn ($value) => $value,
set: fn ($value) => strtolower($value),
);
}
public function markEmailAsVerified()
{
$this->email_verified_at = now();
$this->save();
activity()
->performedOn($this)
->log('email_verified');
}
// API Token Management
public function createSecureToken($name = 'api-token', $abilities = ['*'])
{
$token = $this->createToken($name, $abilities);
activity()
->performedOn($this)
->withProperties(['token_name' => $name])
->log('api_token_created');
return $token;
}
public function revokeAllTokens()
{
$count = $this->tokens()->count();
$this->tokens()->delete();
activity()
->performedOn($this)
->withProperties(['tokens_revoked' => $count])
->log('all_tokens_revoked');
}
// Security Scopes
public function scopeActive($query)
{
return $query->where('is_active', true)->whereNull('deleted_at');
}
public function scopeVerified($query)
{
return $query->whereNotNull('email_verified_at');
}
public function scopeRecentlyActive($query, $days = 30)
{
return $query->where('last_login_at', '>=', now()->subDays($days));
}
}
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
class ThrottleLogins
{
public function handle(Request $request, Closure $next, $maxAttempts = 5, $decayMinutes = 60)
{
$key = $this->throttleKey($request);
if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
$seconds = RateLimiter::availableIn($key);
activity()
->withProperties([
'ip' => $request->ip(),
'email' => $request->input('email'),
'attempts' => RateLimiter::attempts($key),
])
->log('login_throttled');
return response()->json([
'message' => 'Too many login attempts. Please try again in ' . ceil($seconds / 60) . ' minutes.',
'retry_after' => $seconds,
], 429);
}
$response = $next($request);
// Clear attempts on successful login
if ($response->isSuccessful() && $request->routeIs('login')) {
RateLimiter::clear($key);
} else {
// Increment attempts on failed login
RateLimiter::hit($key, $decayMinutes * 60);
}
return $response;
}
protected function throttleKey(Request $request)
{
return Str::lower($request->input('email')).'|'.$request->ip();
}
}
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
class AccountLockoutService
{
const MAX_ATTEMPTS = 5;
const LOCKOUT_DURATION = 30; // minutes
const TEMP_LOCKOUT_DURATION = 5; // minutes for first few attempts
public static function recordFailedAttempt($email)
{
$key = self::getLockoutKey($email);
$attempts = Cache::get($key, 0) + 1;
if ($attempts >= self::MAX_ATTEMPTS) {
self::lockAccount($email);
} else {
$duration = $attempts >= 3 ? self::TEMP_LOCKOUT_DURATION : 1;
Cache::put($key, $attempts, now()->addMinutes($duration));
}
activity()
->withProperties([
'email' => $email,
'attempts' => $attempts,
'ip' => request()->ip(),
])
->log('failed_login_attempt');
return $attempts;
}
public static function lockAccount($email)
{
$key = self::getLockoutKey($email);
Cache::put($key, self::MAX_ATTEMPTS, now()->addMinutes(self::LOCKOUT_DURATION));
// Disable user account if exists
$user = User::where('email', $email)->first();
if ($user) {
$user->suspend('Account locked due to multiple failed login attempts');
}
activity()
->withProperties([
'email' => $email,
'lockout_duration' => self::LOCKOUT_DURATION,
'ip' => request()->ip(),
])
->log('account_locked');
}
public static function isLocked($email)
{
$key = self::getLockoutKey($email);
return Cache::get($key, 0) >= self::MAX_ATTEMPTS;
}
public static function getAttempts($email)
{
return Cache::get(self::getLockoutKey($email), 0);
}
public static function clearAttempts($email)
{
Cache::forget(self::getLockoutKey($email));
activity()
->withProperties(['email' => $email])
->log('login_attempts_cleared');
}
public static function getRemainingLockoutTime($email)
{
if (!self::isLocked($email)) {
return 0;
}
$key = self::getLockoutKey($email);
$expiry = Cache::getStore()->getPrefix() . $key;
// This is implementation specific to your cache driver
return max(0, now()->diffInMinutes(now()->addMinutes(self::LOCKOUT_DURATION)));
}
protected static function getLockoutKey($email)
{
return 'login_attempts:' . strtolower($email);
}
}
<?php
namespace App\Services;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException;
class EncryptionService
{
/**
* Encrypt sensitive data
*/
public static function encryptSensitive($data)
{
if (is_null($data) || $data === '') {
return null;
}
try {
return Crypt::encrypt($data);
} catch (\Exception $e) {
throw new \Exception('Failed to encrypt data: ' . $e->getMessage());
}
}
/**
* Decrypt sensitive data
*/
public static function decryptSensitive($encryptedData)
{
if (is_null($encryptedData) || $encryptedData === '') {
return null;
}
try {
return Crypt::decrypt($encryptedData);
} catch (DecryptException $e) {
throw new \Exception('Failed to decrypt data: ' . $e->getMessage());
}
}
/**
* Hash password with additional security
*/
public static function hashPassword($password)
{
return password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64 MB
'time_cost' => 4, // 4 iterations
'threads' => 3, // 3 threads
]);
}
/**
* Verify password hash
*/
public static function verifyPassword($password, $hash)
{
return password_verify($password, $hash);
}
/**
* Generate secure random token
*/
public static function generateSecureToken($length = 32)
{
return bin2hex(random_bytes($length));
}
/**
* Hash sensitive identifiers (like phone numbers)
*/
public static function hashIdentifier($identifier, $salt = null)
{
$salt = $salt ?: config('app.key');
return hash_hmac('sha256', $identifier, $salt);
}
/**
* Mask sensitive data for display
*/
public static function maskData($data, $type = 'email')
{
if (!$data) return '';
switch ($type) {
case 'email':
$parts = explode('@', $data);
if (count($parts) === 2) {
$username = $parts[0];
$domain = $parts[1];
$maskedUsername = substr($username, 0, 2) . str_repeat('*', max(0, strlen($username) - 2));
return $maskedUsername . '@' . $domain;
}
break;
case 'phone':
$length = strlen($data);
if ($length > 4) {
return str_repeat('*', $length - 4) . substr($data, -4);
}
break;
case 'name':
$words = explode(' ', $data);
if (count($words) > 1) {
return $words[0] . ' ' . substr($words[1], 0, 1) . str_repeat('*', max(0, strlen($words[1]) - 1));
} else {
return substr($data, 0, 2) . str_repeat('*', max(0, strlen($data) - 2));
}
break;
default:
return str_repeat('*', strlen($data));
}
return $data;
}
}
<?php
namespace App\Traits;
use App\Services\EncryptionService;
trait HasEncryptedAttributes
{
/**
* Attributes that should be encrypted
*/
protected $encrypted = [];
/**
* Boot the trait
*/
public static function bootHasEncryptedAttributes()
{
static::saving(function ($model) {
$model->encryptAttributes();
});
static::retrieved(function ($model) {
$model->decryptAttributes();
});
}
/**
* Encrypt specified attributes
*/
protected function encryptAttributes()
{
foreach ($this->getEncryptedAttributes() as $attribute) {
if (isset($this->attributes[$attribute]) && !$this->isEncrypted($this->attributes[$attribute])) {
$this->attributes[$attribute] = EncryptionService::encryptSensitive($this->attributes[$attribute]);
}
}
}
/**
* Decrypt specified attributes
*/
protected function decryptAttributes()
{
foreach ($this->getEncryptedAttributes() as $attribute) {
if (isset($this->attributes[$attribute]) && $this->isEncrypted($this->attributes[$attribute])) {
try {
$this->attributes[$attribute] = EncryptionService::decryptSensitive($this->attributes[$attribute]);
} catch (\Exception $e) {
// Keep encrypted value if decryption fails
\Log::warning("Failed to decrypt attribute {$attribute}: " . $e->getMessage());
}
}
}
}
/**
* Get list of encrypted attributes
*/
protected function getEncryptedAttributes()
{
return $this->encrypted ?? [];
}
/**
* Check if value is encrypted
*/
protected function isEncrypted($value)
{
// Laravel's encryption adds a prefix, so we can check for it
return is_string($value) && strpos($value, 'eyJpdiI6') === 0;
}
/**
* Set an encrypted attribute
*/
public function setEncryptedAttribute($key, $value)
{
$this->attributes[$key] = EncryptionService::encryptSensitive($value);
}
/**
* Get a decrypted attribute
*/
public function getDecryptedAttribute($key)
{
$value = $this->attributes[$key] ?? null;
if ($value && $this->isEncrypted($value)) {
return EncryptionService::decryptSensitive($value);
}
return $value;
}
}
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class NoScriptTag implements Rule
{
public function passes($attribute, $value)
{
return !preg_match('/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi', $value);
}
public function message()
{
return 'The :attribute field contains disallowed script tags.';
}
}
class NoSqlInjection implements Rule
{
protected $sqlKeywords = [
'select', 'insert', 'update', 'delete', 'drop', 'create', 'alter',
'exec', 'execute', 'union', 'declare', 'cast', 'convert'
];
public function passes($attribute, $value)
{
if (!is_string($value)) {
return true;
}
$value = strtolower($value);
foreach ($this->sqlKeywords as $keyword) {
if (strpos($value, $keyword) !== false) {
return false;
}
}
return true;
}
public function message()
{
return 'The :attribute field contains potentially dangerous content.';
}
}
class StrongPassword implements Rule
{
public function passes($attribute, $value)
{
return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/', $value);
}
public function message()
{
return 'The :attribute must be at least 8 characters and contain uppercase, lowercase, number and special character.';
}
}
class SafeFileName implements Rule
{
public function passes($attribute, $value)
{
// Check for directory traversal
if (strpos($value, '..') !== false) {
return false;
}
// Check for dangerous characters
if (preg_match('/[<>:"|?*\\\\\/]/', $value)) {
return false;
}
// Check for executable extensions
$dangerousExtensions = ['exe', 'bat', 'cmd', 'scr', 'pif', 'vbs', 'js', 'jar', 'com'];
$extension = strtolower(pathinfo($value, PATHINFO_EXTENSION));
return !in_array($extension, $dangerousExtensions);
}
public function message()
{
return 'The :attribute contains invalid or dangerous characters.';
}
}
<?php
namespace App\Services;
use HTMLPurifier;
use HTMLPurifier_Config;
class SanitizationService
{
/**
* Sanitize HTML content
*/
public static function sanitizeHtml($content, $allowedTags = null)
{
$config = HTMLPurifier_Config::createDefault();
if ($allowedTags) {
$config->set('HTML.Allowed', $allowedTags);
} else {
// Default allowed tags
$config->set('HTML.Allowed', 'p,br,strong,em,u,ol,ul,li,a[href],img[src|alt],h1,h2,h3,h4,h5,h6');
}
$config->set('AutoFormat.RemoveEmpty', true);
$config->set('AutoFormat.RemoveSpansWithoutAttributes', true);
$purifier = new HTMLPurifier($config);
return $purifier->purify($content);
}
/**
* Sanitize text content (strip all HTML)
*/
public static function sanitizeText($text)
{
return strip_tags(trim($text));
}
/**
* Sanitize email
*/
public static function sanitizeEmail($email)
{
$email = strtolower(trim($email));
return filter_var($email, FILTER_SANITIZE_EMAIL);
}
/**
* Sanitize phone number
*/
public static function sanitizePhone($phone)
{
// Remove all non-numeric characters except +
return preg_replace('/[^0-9+]/', '', $phone);
}
/**
* Sanitize filename
*/
public static function sanitizeFilename($filename)
{
// Remove directory traversal
$filename = basename($filename);
// Remove dangerous characters
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename);
// Limit length
if (strlen($filename) > 255) {
$filename = substr($filename, 0, 255);
}
return $filename;
}
/**
* Sanitize URL
*/
public static function sanitizeUrl($url)
{
$url = trim($url);
// Add protocol if missing
if (!preg_match('/^https?:\/\//', $url)) {
$url = 'http://' . $url;
}
return filter_var($url, FILTER_SANITIZE_URL);
}
/**
* Remove XSS attempts
*/
public static function removeXss($data)
{
if (is_array($data)) {
return array_map([self::class, 'removeXss'], $data);
}
if (!is_string($data)) {
return $data;
}
// Remove null bytes
$data = str_replace("\0", '', $data);
// Remove dangerous characters
$data = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $data);
// Remove script tags
$data = preg_replace('/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi', '', $data);
// Remove javascript: urls
$data = preg_replace('/javascript:/i', '', $data);
// Remove on* events
$data = preg_replace('/\s*on\w+\s*=\s*["\'][^"\']*["\']/i', '', $data);
return $data;
}
/**
* Escape output for safe display
*/
public static function escapeOutput($data, $encoding = 'UTF-8')
{
if (is_array($data)) {
return array_map(function($item) use ($encoding) {
return self::escapeOutput($item, $encoding);
}, $data);
}
return htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, $encoding);
}
}
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ContentSecurityPolicy
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
$csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://maps.googleapis.com",
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com",
"img-src 'self' data: https: blob:",
"connect-src 'self' https://api.qrserver.com https://wa.me https://maps.googleapis.com",
"frame-src 'self' https://www.google.com",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests"
];
$response->headers->set('Content-Security-Policy', implode('; ', $csp));
return $response;
}
}
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class SecurityHeaders
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
// Prevent XSS attacks
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Prevent MIME type sniffing
$response->headers->set('X-Content-Type-Options', 'nosniff');
// Prevent clickjacking
$response->headers->set('X-Frame-Options', 'DENY');
// Referrer policy
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Feature policy
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// HSTS (only in production with HTTPS)
if (app()->environment('production') && $request->isSecure()) {
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
// Remove server information
$response->headers->remove('Server');
$response->headers->remove('X-Powered-By');
return $response;
}
}
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
class ApiRateLimit
{
public function handle(Request $request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if (RateLimiter::tooManyAttempts($key, $maxAttempts)) {
$seconds = RateLimiter::availableIn($key);
// Log rate limit exceeded
activity()
->withProperties([
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'endpoint' => $request->path(),
'method' => $request->method(),
])
->log('api_rate_limit_exceeded');
return response()->json([
'error' => 'Rate limit exceeded',
'message' => 'Too many requests. Please try again later.',
'retry_after' => $seconds,
], 429);
}
RateLimiter::hit($key, $decayMinutes * 60);
$response = $next($request);
// Add rate limit headers
$response->headers->add([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => RateLimiter::remaining($key, $maxAttempts),
'X-RateLimit-Reset' => now()->addSeconds(RateLimiter::availableIn($key))->timestamp,
]);
return $response;
}
protected function resolveRequestSignature(Request $request)
{
$user = $request->user();
if ($user) {
return 'api:' . $user->id;
}
return 'api:' . $request->ip();
}
}
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Request;
class SecurityAuditService
{
/**
* Log security event
*/
public static function logSecurityEvent($event, $data = [], $userId = null, $severity = 'info')
{
DB::table('security_audit_log')->insert([
'user_id' => $userId ?: auth()->id(),
'event' => $event,
'severity' => $severity,
'ip_address' => Request::ip(),
'user_agent' => Request::userAgent(),
'data' => json_encode($data),
'created_at' => now(),
]);
}
/**
* Log authentication events
*/
public static function logAuth($event, $email = null, $success = true)
{
self::logSecurityEvent("auth.{$event}", [
'email' => $email,
'success' => $success,
'session_id' => session()->getId(),
], null, $success ? 'info' : 'warning');
}
/**
* Log permission violations
*/
public static function logPermissionViolation($action, $resource = null, $userId = null)
{
self::logSecurityEvent('permission.violation', [
'action' => $action,
'resource' => $resource,
'route' => Request::route()?->getName(),
'url' => Request::fullUrl(),
], $userId, 'warning');
}
/**
* Log data access
*/
public static function logDataAccess($resource, $action, $resourceId = null)
{
self::logSecurityEvent('data.access', [
'resource' => $resource,
'action' => $action,
'resource_id' => $resourceId,
]);
}
/**
* Log suspicious activity
*/
public static function logSuspiciousActivity($description, $data = [])
{
self::logSecurityEvent('suspicious.activity', array_merge([
'description' => $description,
], $data), null, 'critical');
}
/**
* Get recent security events
*/
public static function getRecentEvents($limit = 100, $severity = null)
{
$query = DB::table('security_audit_log')
->orderBy('created_at', 'desc')
->limit($limit);
if ($severity) {
$query->where('severity', $severity);
}
return $query->get();
}
/**
* Get security metrics
*/
public static function getSecurityMetrics($days = 30)
{
$startDate = now()->subDays($days);
return [
'total_events' => DB::table('security_audit_log')
->where('created_at', '>=', $startDate)
->count(),
'failed_logins' => DB::table('security_audit_log')
->where('event', 'auth.login')
->where('created_at', '>=', $startDate)
->whereJsonContains('data->success', false)
->count(),
'permission_violations' => DB::table('security_audit_log')
->where('event', 'permission.violation')
->where('created_at', '>=', $startDate)
->count(),
'suspicious_activities' => DB::table('security_audit_log')
->where('event', 'suspicious.activity')
->where('created_at', '>=', $startDate)
->count(),
'by_severity' => DB::table('security_audit_log')
->select('severity', DB::raw('count(*) as count'))
->where('created_at', '>=', $startDate)
->groupBy('severity')
->pluck('count', 'severity')
->toArray(),
];
}
}
<?php
// database/migrations/xxxx_create_security_audit_log_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('security_audit_log', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
$table->string('event');
$table->enum('severity', ['info', 'warning', 'error', 'critical'])->default('info');
$table->ipAddress('ip_address')->nullable();
$table->text('user_agent')->nullable();
$table->json('data')->nullable();
$table->timestamp('created_at');
$table->index(['event', 'created_at']);
$table->index(['severity', 'created_at']);
$table->index(['user_id', 'created_at']);
});
}
public function down()
{
Schema::dropIfExists('security_audit_log');
}
};
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class SecureFileUploadService
{
const ALLOWED_IMAGE_TYPES = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const ALLOWED_DOCUMENT_TYPES = ['pdf', 'doc', 'docx', 'txt'];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
/**
* Upload file securely
*/
public static function upload(UploadedFile $file, $directory = 'uploads', $allowedTypes = null)
{
// Validate file
self::validateFile($file, $allowedTypes);
// Generate secure filename
$filename = self::generateSecureFilename($file);
// Store file
$path = $file->storeAs($directory, $filename, 'secure');
// Scan for malware (if available)
if (self::hasAntivirusSupport()) {
self::scanFile(storage_path('app/secure/' . $path));
}
SecurityAuditService::logSecurityEvent('file.upload', [
'filename' => $filename,
'original_name' => $file->getClientOriginalName(),
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'directory' => $directory,
]);
return $path;
}
/**
* Validate uploaded file
*/
protected static function validateFile(UploadedFile $file, $allowedTypes = null)
{
// Check file size
if ($file->getSize() > self::MAX_FILE_SIZE) {
throw new \Exception('File size exceeds maximum allowed size');
}
// Check file extension
$extension = strtolower($file->getClientOriginalExtension());
$allowedTypes = $allowedTypes ?: self::ALLOWED_IMAGE_TYPES;
if (!in_array($extension, $allowedTypes)) {
throw new \Exception('File type not allowed');
}
// Check MIME type
$mimeType = $file->getMimeType();
if (!self::isValidMimeType($mimeType, $extension)) {
throw new \Exception('Invalid file type');
}
// Check for executable content
if (self::containsExecutableContent($file)) {
throw new \Exception('File contains potentially dangerous content');
}
}
/**
* Generate secure filename
*/
protected static function generateSecureFilename(UploadedFile $file)
{
$extension = $file->getClientOriginalExtension();
$hash = hash('sha256', $file->getContent() . time());
return substr($hash, 0, 32) . '.' . strtolower($extension);
}
/**
* Check if MIME type matches extension
*/
protected static function isValidMimeType($mimeType, $extension)
{
$validMimes = [
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'gif' => ['image/gif'],
'webp' => ['image/webp'],
'pdf' => ['application/pdf'],
'doc' => ['application/msword'],
'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'txt' => ['text/plain'],
];
return isset($validMimes[$extension]) && in_array($mimeType, $validMimes[$extension]);
}
/**
* Check for executable content in file
*/
protected static function containsExecutableContent(UploadedFile $file)
{
$content = $file->getContent();
// Check for common executable signatures
$dangerousSignatures = [
'#!/bin/sh',
'#!/bin/bash',
'<?php',
'<script',
'eval(',
'exec(',
'system(',
'shell_exec(',
'MZ', // PE header
'\x7fELF', // ELF header
];
foreach ($dangerousSignatures as $signature) {
if (strpos($content, $signature) !== false) {
return true;
}
}
return false;
}
/**
* Check if antivirus support is available
*/
protected static function hasAntivirusSupport()
{
return extension_loaded('clamav') || class_exists('ClamAV');
}
/**
* Scan file for malware
*/
protected static function scanFile($filepath)
{
// This is a placeholder - implement based on your antivirus solution
// Example: ClamAV integration
if (self::hasAntivirusSupport()) {
// Implement actual malware scanning
return true;
}
return true;
}
/**
* Get file securely
*/
public static function getSecureFile($path)
{
if (!Storage::disk('secure')->exists($path)) {
throw new \Exception('File not found');
}
SecurityAuditService::logDataAccess('file', 'download', $path);
return Storage::disk('secure')->path($path);
}
/**
* Delete file securely
*/
public static function deleteSecureFile($path)
{
if (Storage::disk('secure')->exists($path)) {
$deleted = Storage::disk('secure')->delete($path);
SecurityAuditService::logSecurityEvent('file.delete', [
'path' => $path,
'success' => $deleted,
]);
return $deleted;
}
return false;
}
}
<?php
return [
'driver' => env('SESSION_DRIVER', 'database'),
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => true,
'encrypt' => true,
'files' => storage_path('framework/sessions'),
'connection' => env('SESSION_CONNECTION', null),
'table' => 'sessions',
'store' => env('SESSION_STORE', null),
'lottery' => [2, 100],
'cookie' => env('SESSION_COOKIE', Str::slug(env('APP_NAME', 'laravel'), '_').'_session'),
'path' => '/',
'domain' => env('SESSION_DOMAIN', null),
'secure' => env('SESSION_SECURE_COOKIE', true),
'http_only' => true,
'same_site' => 'strict',
'partitioned' => false,
];
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Http\Middleware\SecurityHeaders;
use App\Http\Middleware\ContentSecurityPolicy;
use App\Http\Middleware\ApiRateLimit;
use App\Services\SecurityAuditService;
class SecurityServiceProvider extends ServiceProvider
{
public function register()
{
// Register security services
$this->app->singleton(SecurityAuditService::class);
}
public function boot()
{
// Global middleware
$this->app['router']->middlewareGroup('security', [
SecurityHeaders::class,
ContentSecurityPolicy::class,
]);
// API middleware
$this->app['router']->middlewareGroup('api-secure', [
'throttle:api',
ApiRateLimit::class,
]);
// Log security events
$this->registerSecurityEventListeners();
}
protected function registerSecurityEventListeners()
{
// Login events
\Event::listen('Illuminate\Auth\Events\Login', function ($event) {
$event->user->recordLogin();
SecurityAuditService::logAuth('login', $event->user->email, true);
});
\Event::listen('Illuminate\Auth\Events\Failed', function ($event) {
SecurityAuditService::logAuth('login', $event->credentials['email'] ?? null, false);
});
\Event::listen('Illuminate\Auth\Events\Logout', function ($event) {
SecurityAuditService::logAuth('logout', $event->user->email, true);
});
// Password reset events
\Event::listen('Illuminate\Auth\Events\PasswordReset', function ($event) {
SecurityAuditService::logSecurityEvent('password.reset', [
'email' => $event->user->email,
], $event->user->id);
});
}
}
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\SecurityAuditService;
use App\Models\User;
use App\Models\Admin;
class SecurityScan extends Command
{
protected $signature = 'security:scan {--days=7 : Number of days to analyze}';
protected $description = 'Perform security analysis and generate report';
public function handle()
{
$days = $this->option('days');
$this->info("๐ Starting security scan for the last {$days} days...");
$this->newLine();
// Get security metrics
$metrics = SecurityAuditService::getSecurityMetrics($days);
// Display summary
$this->displaySecuritySummary($metrics);
// Check for suspicious activities
$this->checkSuspiciousActivities($days);
// Check user accounts
$this->checkUserAccounts();
// Check failed logins
$this->checkFailedLogins($days);
$this->newLine();
$this->info('โ
Security scan completed');
return 0;
}
protected function displaySecuritySummary($metrics)
{
$this->info('๐ Security Summary:');
$this->table(
['Metric', 'Count'],
[
['Total Security Events', number_format($metrics['total_events'])],
['Failed Logins', number_format($metrics['failed_logins'])],
['Permission Violations', number_format($metrics['permission_violations'])],
['Suspicious Activities', number_format($metrics['suspicious_activities'])],
]
);
if (!empty($metrics['by_severity'])) {
$this->newLine();
$this->info('๐ Events by Severity:');
foreach ($metrics['by_severity'] as $severity => $count) {
$icon = match($severity) {
'critical' => '๐ด',
'error' => '๐ ',
'warning' => '๐ก',
default => '๐ข'
};
$this->line(" {$icon} {$severity}: {$count}");
}
}
}
protected function checkSuspiciousActivities($days)
{
$suspiciousEvents = SecurityAuditService::getRecentEvents(50, 'critical');
if ($suspiciousEvents->isNotEmpty()) {
$this->newLine();
$this->warn('โ ๏ธ Suspicious Activities Detected:');
foreach ($suspiciousEvents->take(10) as $event) {
$data = json_decode($event->data, true);
$this->line(" โข {$event->created_at}: {$data['description'] ?? $event->event}");
}
if ($suspiciousEvents->count() > 10) {
$this->line(" ... and " . ($suspiciousEvents->count() - 10) . " more");
}
}
}
protected function checkUserAccounts()
{
// Check for inactive admin accounts
$inactiveAdmins = Admin::where('last_login_at', '<', now()->subDays(90))
->orWhereNull('last_login_at')
->count();
// Check for unverified users
$unverifiedUsers = User::whereNull('email_verified_at')
->where('created_at', '<', now()->subDays(7))
->count();
// Check for suspended accounts
$suspendedUsers = User::where('is_active', false)->count();
$this->newLine();
$this->info('๐ฅ Account Security:');
$this->table(
['Account Type', 'Count', 'Status'],
[
['Inactive Admins (90+ days)', $inactiveAdmins, $inactiveAdmins > 0 ? 'โ ๏ธ Review' : 'โ
OK'],
['Unverified Users (7+ days)', $unverifiedUsers, $unverifiedUsers > 10 ? 'โ ๏ธ Review' : 'โ
OK'],
['Suspended Accounts', $suspendedUsers, $suspendedUsers > 0 ? 'โน๏ธ Monitor' : 'โ
OK'],
]
);
}
protected function checkFailedLogins($days)
{
$failedLogins = \DB::table('security_audit_log')
->where('event', 'auth.login')
->where('created_at', '>=', now()->subDays($days))
->whereJsonContains('data->success', false)
->select('ip_address', \DB::raw('count(*) as attempts'))
->groupBy('ip_address')
->having('attempts', '>', 10)
->orderByDesc('attempts')
->get();
if ($failedLogins->isNotEmpty()) {
$this->newLine();
$this->warn('๐จ High Failed Login Attempts:');
$this->table(
['IP Address', 'Failed Attempts'],
$failedLogins->map(function($item) {
return [$item->ip_address, $item->attempts];
})->toArray()
);
}
}
}
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\AccountLockoutService;
use Illuminate\Support\Facades\DB;
class SecurityCleanup extends Command
{
protected $signature = 'security:cleanup {--days=30 : Days to keep audit logs}';
protected $description = 'Clean up old security logs and reset counters';
public function handle()
{
$days = $this->option('days');
$this->info("๐งน Starting security cleanup...");
// Clean old audit logs
$deletedLogs = $this->cleanAuditLogs($days);
// Clean old sessions
$deletedSessions = $this->cleanOldSessions();
// Reset failed login attempts for resolved IPs
$this->resetLoginAttempts();
$this->newLine();
$this->info("โ
Security cleanup completed:");
$this->line(" โข Deleted {$deletedLogs} old audit log entries");
$this->line(" โข Deleted {$deletedSessions} old sessions");
return 0;
}
protected function cleanAuditLogs($days)
{
$cutoffDate = now()->subDays($days);
return DB::table('security_audit_log')
->where('created_at', '<', $cutoffDate)
->where('severity', '!=', 'critical') // Keep critical events longer
->delete();
}
protected function cleanOldSessions()
{
return DB::table('sessions')
->where('last_activity', '<', now()->subDays(30)->timestamp)
->delete();
}
protected function resetLoginAttempts()
{
// This would depend on your cache implementation
// Example for Redis:
$this->info(" โข Reset login attempt counters");
}
}
Next: Performance Optimization untuk optimasi performa sistem.