Security Strategy
Executive Summary
This document outlines comprehensive API security measures for the Wedissimo marketplace migration, addressing OWASP API Security Top 10 (2025) compliance, rate limiting, and marketplace-specific threats. The strategy balances robust security with marketplace functionality and performance requirements for the Laravel backend and Next.js frontend architecture.
OWASP API Security Top 10 (2025) Compliance
1. Broken Object Level Authorization (BOLA)
Risk: Users accessing other users' bookings, vendor data, or private information.
Implementation:
// Laravel Policy-based authorization
class BookingPolicy
{
public function view(User $user, Booking $booking)
{
// Couples can only view their own bookings
if ($user->role === 'couple') {
return $user->id === $booking->user_id;
}
// Vendors can only view bookings for their services
if ($user->role === 'vendor') {
return $user->vendor->id === $booking->vendor_id;
}
// Admins can view all bookings
return $user->role === 'admin';
}
public function update(User $user, Booking $booking)
{
// Only couples can update their own bookings (within constraints)
if ($user->role === 'couple') {
return $user->id === $booking->user_id &&
$booking->status === 'pending' &&
$booking->wedding_date->isFuture();
}
return false; // Vendors cannot modify bookings directly
}
}
// API Controller with automatic authorization
class BookingController extends Controller
{
public function show(Booking $booking)
{
$this->authorize('view', $booking);
return new BookingResource($booking);
}
public function update(UpdateBookingRequest $request, Booking $booking)
{
$this->authorize('update', $booking);
// Process update...
}
}2. Broken Authentication
Risk: Weak authentication allowing unauthorized access to user accounts.
Implementation:
// Multi-factor authentication for sensitive actions
class AuthController extends Controller
{
public function login(LoginRequest $request)
{
$credentials = $request->validated();
// Rate limiting on login attempts
if (RateLimiter::tooManyAttempts('login:' . $request->ip(), 5)) {
throw ValidationException::withMessages([
'email' => 'Too many login attempts. Please try again in 15 minutes.'
]);
}
if (Auth::attempt($credentials)) {
$user = Auth::user();
// Require MFA for vendors and admins
if (in_array($user->role, ['vendor', 'admin']) && !$user->hasVerifiedMFA()) {
return response()->json([
'message' => 'MFA verification required',
'mfa_token' => $user->generateMFAToken()
], 202);
}
// Generate secure API token
$token = $user->createToken('wedissimo-app', [
'bookings:read',
'bookings:write',
'messages:read',
'messages:write'
])->plainTextToken;
RateLimiter::clear('login:' . $request->ip());
return response()->json([
'user' => new UserResource($user),
'token' => $token,
'expires_at' => now()->addHours(24)
]);
}
RateLimiter::hit('login:' . $request->ip(), 900); // 15 minutes
throw ValidationException::withMessages([
'email' => 'Invalid credentials'
]);
}
public function verifyMFA(MFAVerificationRequest $request)
{
$user = $request->user();
if (!$user->verifyMFACode($request->code)) {
throw ValidationException::withMessages([
'code' => 'Invalid MFA code'
]);
}
$user->markMFAAsVerified();
// Generate full access token after MFA
$token = $user->createToken('wedissimo-app-verified')->plainTextToken;
return response()->json([
'token' => $token,
'message' => 'MFA verified successfully'
]);
}
}3. Broken Object Property Level Authorization
Risk: Users modifying fields they shouldn't have access to.
Implementation:
// Role-based field access control
class UserUpdateRequest extends FormRequest
{
public function authorize()
{
return $this->user()->id === $this->route('user')->id ||
$this->user()->role === 'admin';
}
public function rules()
{
$rules = [
'display_name' => 'sometimes|string|max:255',
'bio' => 'sometimes|string|max:1000',
'wedding_date' => 'sometimes|date|after:today',
];
// Only admins can modify these fields
if ($this->user()->role === 'admin') {
$rules['role'] = 'sometimes|in:couple,vendor,admin';
$rules['status'] = 'sometimes|in:active,suspended,banned';
$rules['verified'] = 'sometimes|boolean';
}
// Vendors can modify business-specific fields
if ($this->user()->role === 'vendor') {
$rules['business_name'] = 'sometimes|string|max:255';
$rules['service_category'] = 'sometimes|string|in:photography,videography,catering,venue,music';
$rules['price_range'] = 'sometimes|array';
}
return $rules;
}
protected function prepareForValidation()
{
// Remove unauthorized fields before validation
$allowedFields = $this->getAllowedFields();
$this->replace(array_intersect_key($this->all(), $allowedFields));
}
private function getAllowedFields()
{
$baseFields = ['display_name', 'bio', 'wedding_date'];
if ($this->user()->role === 'admin') {
return array_merge($baseFields, ['role', 'status', 'verified']);
}
if ($this->user()->role === 'vendor') {
return array_merge($baseFields, ['business_name', 'service_category', 'price_range']);
}
return $baseFields;
}
}4. Unrestricted Resource Consumption
Risk: API abuse leading to service degradation or costs.
Implementation:
// Comprehensive rate limiting strategy
class RateLimitingMiddleware
{
public function handle($request, Closure $next)
{
$user = $request->user();
$endpoint = $request->route()->getName();
// Different limits for different user types and endpoints
$limits = $this->getRateLimits($user, $endpoint);
foreach ($limits as $key => $limit) {
if (RateLimiter::tooManyAttempts($key, $limit['attempts'])) {
return response()->json([
'message' => 'Rate limit exceeded',
'retry_after' => RateLimiter::availableIn($key)
], 429);
}
RateLimiter::hit($key, $limit['decay']);
}
return $next($request);
}
private function getRateLimits($user, $endpoint)
{
$userId = $user ? $user->id : request()->ip();
$userType = $user ? $user->role : 'guest';
$limits = [];
// Per-user limits
$limits["user:{$userId}"] = [
'attempts' => $this->getUserLimits($userType),
'decay' => 60 // 1 minute
];
// Per-endpoint limits
$limits["endpoint:{$endpoint}:{$userId}"] = [
'attempts' => $this->getEndpointLimits($endpoint, $userType),
'decay' => 60
];
// Global IP limits (DDoS protection)
$limits["ip:" . request()->ip()] = [
'attempts' => 1000,
'decay' => 60
];
// Search-specific limits (expensive operations)
if (str_contains($endpoint, 'search')) {
$limits["search:{$userId}"] = [
'attempts' => 30,
'decay' => 60
];
}
return $limits;
}
private function getUserLimits($userType)
{
return match($userType) {
'admin' => 500,
'vendor' => 300,
'couple' => 200,
'guest' => 50,
default => 50
};
}
private function getEndpointLimits($endpoint, $userType)
{
$endpointLimits = [
'vendor.search' => ['admin' => 100, 'vendor' => 50, 'couple' => 100, 'guest' => 20],
'bookings.create' => ['admin' => 50, 'vendor' => 0, 'couple' => 10, 'guest' => 0],
'messages.send' => ['admin' => 100, 'vendor' => 50, 'couple' => 50, 'guest' => 0],
'auth.login' => ['guest' => 5], // Very restrictive for login attempts
];
return $endpointLimits[$endpoint][$userType] ?? 60;
}
}5. Broken Function Level Authorization
Risk: Users accessing administrative or vendor-only functions.
Implementation:
// Middleware for function-level authorization
class RoleMiddleware
{
public function handle($request, Closure $next, ...$roles)
{
if (!$request->user()) {
return response()->json(['message' => 'Unauthenticated'], 401);
}
if (!in_array($request->user()->role, $roles)) {
return response()->json([
'message' => 'Insufficient permissions',
'required_roles' => $roles,
'user_role' => $request->user()->role
], 403);
}
return $next($request);
}
}
// Route definitions with role protection
Route::middleware(['auth:sanctum', 'role:admin'])->group(function () {
Route::get('/admin/analytics', [AdminController::class, 'analytics']);
Route::post('/admin/vendors/{vendor}/approve', [AdminController::class, 'approveVendor']);
Route::get('/admin/disputes', [AdminController::class, 'disputes']);
});
Route::middleware(['auth:sanctum', 'role:vendor'])->group(function () {
Route::put('/vendor/listings/{listing}', [VendorController::class, 'updateListing']);
Route::post('/vendor/availability', [VendorController::class, 'updateAvailability']);
});
Route::middleware(['auth:sanctum', 'role:couple,admin'])->group(function () {
Route::post('/bookings', [BookingController::class, 'create']);
Route::get('/bookings/{booking}/invoice', [BookingController::class, 'invoice']);
});6. Server-Side Request Forgery (SSRF)
Risk: API accepting URLs that could access internal services.
Implementation:
// SSRF protection for URL validation
class UrlValidationService
{
private const BLOCKED_HOSTS = [
'localhost',
'127.0.0.1',
'0.0.0.0',
'169.254.169.254', // AWS metadata
'::1',
];
private const BLOCKED_PORTS = [22, 23, 25, 53, 80, 135, 139, 445, 993, 995];
private const ALLOWED_SCHEMES = ['https'];
public function validateUrl(string $url): bool
{
$parsed = parse_url($url);
if (!$parsed || !isset($parsed['scheme'], $parsed['host'])) {
return false;
}
// Only allow HTTPS
if (!in_array($parsed['scheme'], self::ALLOWED_SCHEMES)) {
return false;
}
// Block internal/private networks
if ($this->isBlockedHost($parsed['host'])) {
return false;
}
// Block dangerous ports
if (isset($parsed['port']) && in_array($parsed['port'], self::BLOCKED_PORTS)) {
return false;
}
// Resolve DNS to check for private IPs
$ip = gethostbyname($parsed['host']);
if ($this->isPrivateIp($ip)) {
return false;
}
return true;
}
private function isBlockedHost(string $host): bool
{
return in_array(strtolower($host), self::BLOCKED_HOSTS) ||
str_contains(strtolower($host), 'internal') ||
str_contains(strtolower($host), 'local');
}
private function isPrivateIp(string $ip): bool
{
return !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}
}
// Usage in vendor profile update
class VendorProfileRequest extends FormRequest
{
public function rules()
{
return [
'website_url' => ['sometimes', 'url', new ValidPublicUrl],
'portfolio_urls.*' => ['url', new ValidPublicUrl],
'social_media.instagram' => ['sometimes', 'url', new ValidPublicUrl],
];
}
}
class ValidPublicUrl implements Rule
{
public function passes($attribute, $value)
{
return app(UrlValidationService::class)->validateUrl($value);
}
public function message()
{
return 'The :attribute must be a valid public HTTPS URL.';
}
}7. Security Misconfiguration
Risk: Default configurations, verbose error messages, missing security headers.
Implementation:
// Security headers middleware
class SecurityHeadersMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);
return $response->withHeaders([
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'DENY',
'X-XSS-Protection' => '1; mode=block',
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
'Content-Security-Policy' => $this->getCSPHeader(),
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Permissions-Policy' => 'camera=(), microphone=(), geolocation=()',
]);
}
private function getCSPHeader(): string
{
$policies = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://js.stripe.com https://meet.google.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https: blob:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.stripe.com https://meet.google.com",
"frame-src https://js.stripe.com https://meet.google.com",
"object-src 'none'",
"base-uri 'self'",
];
return implode('; ', $policies);
}
}
// Environment-specific error handling
class ApiExceptionHandler extends Handler
{
public function render($request, Throwable $exception)
{
if ($request->expectsJson()) {
return $this->handleApiException($request, $exception);
}
return parent::render($request, $exception);
}
private function handleApiException($request, Throwable $exception)
{
if ($exception instanceof ValidationException) {
return response()->json([
'message' => 'Validation failed',
'errors' => $exception->errors()
], 422);
}
if ($exception instanceof ModelNotFoundException) {
return response()->json([
'message' => 'Resource not found'
], 404);
}
if ($exception instanceof AuthorizationException) {
return response()->json([
'message' => 'Access denied'
], 403);
}
// Production: Hide detailed error information
if (app()->environment('production')) {
return response()->json([
'message' => 'Internal server error',
'error_id' => Str::uuid() // For support tracking
], 500);
}
// Development: Show detailed errors
return response()->json([
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString()
], 500);
}
}8. Lack of Protection from Automated Threats
Risk: Bots, scrapers, and automated attacks.
Implementation:
// Bot detection and protection
class BotProtectionMiddleware
{
public function handle($request, Closure $next)
{
$userAgent = $request->userAgent();
$ip = $request->ip();
// Check against known bot patterns
if ($this->isSuspiciousBot($userAgent)) {
return response()->json(['message' => 'Access denied'], 403);
}
// Implement CAPTCHA for suspicious activity
if ($this->requiresCaptcha($request)) {
if (!$this->verifyCaptcha($request)) {
return response()->json([
'message' => 'CAPTCHA verification required',
'captcha_required' => true
], 429);
}
}
// Rate limiting for automated requests
if ($this->isAutomatedRequest($request)) {
$key = "automated:{$ip}";
if (RateLimiter::tooManyAttempts($key, 10)) {
return response()->json(['message' => 'Too many automated requests'], 429);
}
RateLimiter::hit($key, 300); // 5 minutes
}
return $next($request);
}
private function isSuspiciousBot($userAgent): bool
{
$suspiciousPatterns = [
'scrapy', 'crawler', 'spider', 'bot', 'curl', 'wget',
'python-requests', 'go-http-client', 'okhttp'
];
foreach ($suspiciousPatterns as $pattern) {
if (stripos($userAgent, $pattern) !== false) {
return true;
}
}
return false;
}
private function requiresCaptcha($request): bool
{
$ip = $request->ip();
// Check for rapid requests
$rapidRequestsKey = "rapid_requests:{$ip}";
if (RateLimiter::attempts($rapidRequestsKey) > 30) {
return true;
}
// Check for failed login attempts
$failedLoginsKey = "failed_logins:{$ip}";
if (RateLimiter::attempts($failedLoginsKey) > 3) {
return true;
}
return false;
}
private function verifyCaptcha($request): bool
{
$captchaResponse = $request->input('captcha_response');
if (!$captchaResponse) {
return false;
}
// Verify with reCAPTCHA v3
$response = Http::post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => config('services.recaptcha.secret'),
'response' => $captchaResponse,
'remoteip' => $request->ip()
]);
$result = $response->json();
return $result['success'] && $result['score'] > 0.5;
}
}Web Application Security (Next.js Frontend)
Browser Security Headers
// Enhanced security headers for web application
class WebSecurityHeadersMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);
return $response->withHeaders([
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'DENY',
'X-XSS-Protection' => '1; mode=block',
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload',
'Content-Security-Policy' => $this->getWebCSPHeader(),
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Permissions-Policy' => 'camera=(), microphone=(), geolocation=(), payment=()',
]);
}
private function getWebCSPHeader(): string
{
$policies = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://js.stripe.com https://meet.google.com https://www.google.com https://www.gstatic.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https: blob:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.stripe.com https://meet.google.com https://*.wedissimo.com",
"frame-src https://js.stripe.com https://meet.google.com",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
];
return implode('; ', $policies);
}
}Next.js Client-Side Security
// Secure API client for Next.js frontend
class WedissimoApiClient {
private baseURL: string;
private token: string | null = null;
constructor() {
this.baseURL = process.env.NEXT_PUBLIC_API_URL!;
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor for authentication
this.addRequestInterceptor((config) => {
if (this.token) {
config.headers.Authorization = `Bearer ${this.token}`;
}
// Add CSRF token for state-changing requests
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(config.method?.toUpperCase() || '')) {
config.headers['X-CSRF-TOKEN'] = this.getCSRFToken();
}
// Add request ID for tracing
config.headers['X-Request-ID'] = this.generateRequestId();
return config;
});
// Response interceptor for error handling
this.addResponseInterceptor(
(response) => response,
(error) => this.handleApiError(error)
);
}
private getCSRFToken(): string {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
}
private generateRequestId(): string {
return `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
private handleApiError(error: any) {
if (error.response?.status === 401) {
// Clear stored token and redirect to login
this.clearToken();
window.location.href = '/login';
}
if (error.response?.status === 429) {
// Rate limited - show user-friendly message
throw new Error('Too many requests. Please wait a moment and try again.');
}
return Promise.reject(error);
}
// Vendor search with built-in security
async searchVendors(criteria: VendorSearchCriteria): Promise<VendorSearchResult> {
// Sanitize search input
const sanitizedCriteria = this.sanitizeSearchInput(criteria);
return this.get('/api/vendors/search', { params: sanitizedCriteria });
}
private sanitizeSearchInput(criteria: any): any {
// Remove potentially dangerous characters
const sanitized = { ...criteria };
Object.keys(sanitized).forEach(key => {
if (typeof sanitized[key] === 'string') {
// Remove script tags and other dangerous content
sanitized[key] = sanitized[key]
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+=/gi, '')
.trim();
}
});
return sanitized;
}
}
## API Input Validation & Sanitization
### Comprehensive Input Validation
```php
// Base request class with security-first validation
abstract class SecureFormRequest extends FormRequest
{
protected function prepareForValidation()
{
// Remove null bytes and control characters
$this->sanitizeInput();
// Trim whitespace
$this->trimStrings();
// Convert empty strings to null
$this->convertEmptyStringsToNull();
}
private function sanitizeInput()
{
$input = $this->all();
array_walk_recursive($input, function (&$value) {
if (is_string($value)) {
// Remove null bytes and control characters
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value);
// Basic XSS prevention
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
});
$this->replace($input);
}
private function trimStrings()
{
$input = $this->all();
array_walk_recursive($input, function (&$value) {
if (is_string($value)) {
$value = trim($value);
}
});
$this->replace($input);
}
private function convertEmptyStringsToNull()
{
$input = $this->all();
array_walk_recursive($input, function (&$value) {
if ($value === '') {
$value = null;
}
});
$this->replace($input);
}
}
// Booking creation with comprehensive validation
class CreateBookingRequest extends SecureFormRequest
{
public function rules()
{
return [
'vendor_id' => [
'required',
'integer',
'exists:vendors,id',
new VendorIsActive,
new VendorAcceptsBookings
],
'wedding_date' => [
'required',
'date',
'after:' . now()->addDays(7)->toDateString(), // Minimum 7 days notice
'before:' . now()->addYears(3)->toDateString(), // Maximum 3 years ahead
new VendorAvailable
],
'venue_location' => [
'required',
'string',
'max:255',
'regex:/^[a-zA-Z0-9\s,.\-\'()]+$/', // Only allow safe characters
],
'guest_count' => [
'required',
'integer',
'min:1',
'max:2000'
],
'budget_range' => [
'required',
'array',
'size:2'
],
'budget_range.min' => [
'required',
'numeric',
'min:100',
'max:100000'
],
'budget_range.max' => [
'required',
'numeric',
'min:100',
'max:100000',
'gte:budget_range.min'
],
'special_requirements' => [
'sometimes',
'string',
'max:1000',
new NoMaliciousContent
],
'contact_preference' => [
'required',
'in:email,phone,whatsapp'
]
];
}
public function messages()
{
return [
'wedding_date.after' => 'Wedding date must be at least 7 days from now',
'wedding_date.before' => 'Wedding date cannot be more than 3 years from now',
'venue_location.regex' => 'Venue location contains invalid characters',
];
}
}
// Custom validation rules
class VendorIsActive implements Rule
{
public function passes($attribute, $value)
{
return Vendor::where('id', $value)
->where('status', 'active')
->where('verified', true)
->exists();
}
public function message()
{
return 'The selected vendor is not available for bookings.';
}
}
class NoMaliciousContent implements Rule
{
private $maliciousPatterns = [
'/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi',
'/javascript:/i',
'/vbscript:/i',
'/on\w+\s*=/i',
'/<iframe\b[^>]*>/i',
'/<object\b[^>]*>/i',
'/<embed\b[^>]*>/i',
];
public function passes($attribute, $value)
{
foreach ($this->maliciousPatterns as $pattern) {
if (preg_match($pattern, $value)) {
return false;
}
}
return true;
}
public function message()
{
return 'The :attribute contains potentially malicious content.';
}
}Implementation Timeline
Week 1: Core API Security Framework
- [ ] Implement OWASP API Top 10 compliance (BOLA, Authentication, Authorization)
- [ ] Set up comprehensive rate limiting middleware
- [ ] Configure security headers and CSRF protection
- [ ] Implement input validation framework
Week 2: Advanced Security Features
- [ ] Deploy bot protection and CAPTCHA integration
- [ ] Implement web application security (CSP headers, client-side sanitization)
- [ ] Set up SSRF protection for URL validation
- [ ] Configure environment-specific error handling
Week 3: Monitoring and Testing
- [ ] Implement security audit logging
- [ ] Set up automated security testing in CI/CD
- [ ] Configure monitoring and alerting for security events
- [ ] Conduct penetration testing of API endpoints
Success Metrics
Security Metrics
- OWASP Compliance: 100% compliance with API Security Top 10
- Attack Prevention: >99% automated attack detection and blocking
- Response Time: <5ms additional latency from security middleware
- False Positives: <1% legitimate requests blocked
Performance Metrics
- API Response Time: Maintain <500ms average response time
- Rate Limiting Accuracy: 100% effective rate limiting without false positives
- Uptime: 99.9% availability despite security controls
- Web Performance: <5ms additional latency from security middleware
This comprehensive API security strategy ensures the Wedissimo marketplace is protected against modern threats while maintaining excellent performance and user experience for the Laravel backend and Next.js frontend architecture.