API Security Audit Checklist for Laravel: OWASP Best Practices
·
7 min read
Introduction
APIs are the main gateway of modern applications and also the most common attack target. This article provides a comprehensive security checklist for Laravel APIs based on OWASP API Security Top 10.
OWASP API Security Top 10 (2023)
- API1:2023 - Broken Object Level Authorization
- API2:2023 - Broken Authentication
- API3:2023 - Broken Object Property Level Authorization
- API4:2023 - Unrestricted Resource Consumption
- API5:2023 - Broken Function Level Authorization
- API6:2023 - Unrestricted Access to Sensitive Business Flows
- API7:2023 - Server Side Request Forgery (SSRF)
- API8:2023 - Security Misconfiguration
- API9:2023 - Improper Inventory Management
- API10:2023 - Unsafe Consumption of APIs
1. Authentication Security
Secure Token Configuration
// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
'guard' => ['web'],
'expiration' => 60 * 24, // 24 hours
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
];
Token Abilities (Scopes)
// app/Http/Controllers/AuthController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required|string',
'device_name' => 'required|string|max:255',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
// Generic error message to prevent user enumeration
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
// Check if account is locked
if ($user->isLocked()) {
throw ValidationException::withMessages([
'email' => ['Account is temporarily locked. Please try again later.'],
]);
}
// Define abilities based on user role
$abilities = $this->getAbilitiesForUser($user);
// Revoke old tokens for this device
$user->tokens()
->where('name', $request->device_name)
->delete();
$token = $user->createToken(
$request->device_name,
$abilities,
now()->addHours(24)
);
return response()->json([
'token' => $token->plainTextToken,
'expires_at' => $token->accessToken->expires_at,
]);
}
private function getAbilitiesForUser(User $user): array
{
return match ($user->role) {
'admin' => ['*'],
'manager' => ['users:read', 'users:write', 'orders:read', 'orders:write'],
'staff' => ['orders:read', 'orders:write'],
default => ['profile:read', 'profile:write'],
};
}
}
Brute Force Protection
// app/Services/LoginAttemptService.php
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
class LoginAttemptService
{
private const MAX_ATTEMPTS = 5;
private const DECAY_MINUTES = 15;
private const LOCKOUT_MINUTES = 60;
public function recordFailedAttempt(string $email, string $ip): void
{
$emailKey = $this->getEmailKey($email);
$ipKey = $this->getIpKey($ip);
RateLimiter::hit($emailKey, self::DECAY_MINUTES * 60);
RateLimiter::hit($ipKey, self::DECAY_MINUTES * 60);
if ($this->tooManyAttempts($email, $ip)) {
$this->lockAccount($email);
}
}
public function tooManyAttempts(string $email, string $ip): bool
{
return RateLimiter::tooManyAttempts($this->getEmailKey($email), self::MAX_ATTEMPTS) ||
RateLimiter::tooManyAttempts($this->getIpKey($ip), self::MAX_ATTEMPTS * 3);
}
private function lockAccount(string $email): void
{
Cache::put("account_locked:{$email}", true, now()->addMinutes(self::LOCKOUT_MINUTES));
Log::channel('security')->warning('Account locked due to too many failed attempts', [
'email' => $email,
]);
}
private function getEmailKey(string $email): string
{
return 'login_attempts:email:' . hash('sha256', strtolower($email));
}
private function getIpKey(string $ip): string
{
return 'login_attempts:ip:' . $ip;
}
}
2. Authorization (BOLA & BFLA Protection)
Policy-Based Authorization
// app/Policies/OrderPolicy.php
<?php
namespace App\Policies;
use App\Models\Order;
use App\Models\User;
class OrderPolicy
{
public function view(User $user, Order $order): bool
{
// Admin can view all
if ($user->isAdmin()) {
return true;
}
// Users can only view their own orders
return $user->id === $order->user_id;
}
public function update(User $user, Order $order): bool
{
if (!$user->tokenCan('orders:write')) {
return false;
}
return $user->id === $order->user_id || $user->isAdmin();
}
public function delete(User $user, Order $order): bool
{
return $user->isAdmin() && $user->tokenCan('orders:delete');
}
}
Prevent Mass Assignment
// app/Models/User.php
class User extends Authenticatable
{
protected $fillable = [
'name',
'email',
'password',
];
// Never allow these to be mass assigned
protected $guarded = [
'id',
'role',
'is_admin',
'email_verified_at',
];
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
];
}
3. Input Validation
Request Validation
// app/Http/Requests/StoreUserRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => [
'required',
'string',
'min:2',
'max:255',
'regex:/^[\pL\s\-\']+$/u',
],
'email' => [
'required',
'string',
'email:rfc,dns',
'max:255',
'unique:users,email',
],
'password' => [
'required',
'string',
Password::min(12)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(3),
'confirmed',
],
];
}
}
SQL Injection Prevention
// ❌ NEVER do this - SQL Injection vulnerable
$users = DB::select("SELECT * FROM users WHERE email = '{$request->email}'");
// ✅ Use parameter binding
$users = DB::select('SELECT * FROM users WHERE email = ?', [$request->email]);
// ✅ Or use Eloquent/Query Builder
$users = User::where('email', $request->email)->get();
4. Rate Limiting
Advanced Rate Limiting
// app/Providers/RouteServiceProvider.php
protected function configureRateLimiting(): void
{
// Default API rate limit
RateLimiter::for('api', function (Request $request) {
$user = $request->user();
if ($user) {
$limit = match ($user->subscription_tier) {
'enterprise' => 10000,
'business' => 5000,
'pro' => 1000,
default => 100,
};
return Limit::perMinute($limit)->by($user->id);
}
return Limit::perMinute(20)->by($request->ip());
});
// Strict rate limit for authentication
RateLimiter::for('auth', function (Request $request) {
return [
Limit::perMinute(5)->by($request->ip()),
Limit::perMinute(10)->by($request->input('email', 'guest')),
];
});
}
5. Security Headers
// app/Http/Middleware/SecurityHeaders.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'");
if ($request->secure()) {
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
$response->headers->remove('X-Powered-By');
$response->headers->remove('Server');
return $response;
}
}
6. Logging & Monitoring
// app/Services/SecurityLogger.php
<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\Request;
class SecurityLogger
{
public function logAuthenticationAttempt(Request $request, bool $success): void
{
$context = [
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'email' => $request->input('email'),
'success' => $success,
'timestamp' => now()->toIso8601String(),
];
if ($success) {
Log::channel('security')->info('Authentication successful', $context);
} else {
Log::channel('security')->warning('Authentication failed', $context);
}
}
public function logSuspiciousActivity(Request $request, string $activity, array $details = []): void
{
Log::channel('security')->alert('Suspicious activity detected', [
'user_id' => $request->user()?->id,
'activity' => $activity,
'details' => $details,
'ip' => $request->ip(),
'timestamp' => now()->toIso8601String(),
]);
}
}
7. SSRF Prevention
// app/Services/SafeHttpClient.php
<?php
namespace App\Services;
class SafeHttpClient
{
private array $blockedHosts = [
'localhost', '127.0.0.1', '::1', '0.0.0.0',
'169.254.169.254', // AWS metadata
];
private array $blockedCidrs = [
'10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16',
'127.0.0.0/8', '169.254.0.0/16',
];
public function validateUrl(string $url): void
{
$parsed = parse_url($url);
if (!$parsed || !isset($parsed['host'])) {
throw new \InvalidArgumentException('Invalid URL');
}
$host = strtolower($parsed['host']);
if (in_array($host, $this->blockedHosts)) {
throw new \InvalidArgumentException('Access to this host is not allowed');
}
$ips = gethostbynamel($host);
foreach ($ips as $ip) {
if ($this->isBlockedIp($ip)) {
throw new \InvalidArgumentException('Access to this IP range is not allowed');
}
}
}
}
8. API Security Checklist
Authentication
- Use strong token-based authentication
- Implement token expiration
- Implement brute force protection
- Support MFA/2FA
Authorization
- Implement resource-level authorization (Policies)
- Check ownership for every resource access
- Prevent mass assignment vulnerabilities
Data Protection
- Use HTTPS everywhere
- Encrypt sensitive data at rest
- Hash passwords with bcrypt/argon2
Rate Limiting
- Apply rate limits to all endpoints
- Stricter limits for authentication endpoints
- Per-user rate limiting
Headers & CORS
- Set all security headers
- Configure CORS properly
- Enable HSTS
Logging & Monitoring
- Log all authentication events
- Log authorization failures
- Monitor for suspicious patterns
Conclusion
API security is an ongoing process. Make sure to:
- Review code regularly
- Update dependencies periodically
- Monitor logs and alerts
- Conduct security audits
- Stay updated with OWASP guidelines