- C1: Hash refresh tokens before DB storage (sha256) - C2: Remove JWT_SECRET fallback, fail hard if missing - H1: Enforce HTTP methods per route (405 on mismatch) - H2: CORS with origin whitelist from CORS_ORIGIN env var - H3: Redact sensitive fields (tokens, passwords) from logs - M1: Build HmacMiddleware with replay attack prevention - M2: Fix rate limiter race condition with flock LOCK_EX - M3: Guard dd() — suppressed in production - M4: Remove .env from git tracking, strengthen .gitignore - I1: Add HSTS header (max-age=31536000)
72 lines
2.0 KiB
PHP
72 lines
2.0 KiB
PHP
<?php
|
|
/**
|
|
* Rate Limiting Middleware (File-based, Race-Condition Safe)
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Middleware;
|
|
|
|
final class RateLimitMiddleware
|
|
{
|
|
/**
|
|
* File-based rate limiter with file-lock to prevent race conditions.
|
|
* For multi-server deployments, replace with Redis.
|
|
*/
|
|
public static function check(int $maxRequests = 60, int $timeWindow = 60): void
|
|
{
|
|
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
|
$cacheDir = STORAGE_PATH . '/cache';
|
|
$cacheFile = $cacheDir . '/rl_' . md5($ip) . '.json';
|
|
|
|
if (!is_dir($cacheDir)) {
|
|
mkdir($cacheDir, 0755, true);
|
|
}
|
|
|
|
// M2 Fix: Use exclusive file lock to prevent race condition
|
|
$fp = fopen($cacheFile, 'c+');
|
|
if ($fp === false) {
|
|
// If we can't open the file, fail open (don't block all users)
|
|
return;
|
|
}
|
|
|
|
try {
|
|
flock($fp, LOCK_EX); // Exclusive lock — blocks until acquired
|
|
|
|
$now = time();
|
|
$content = stream_get_contents($fp);
|
|
$requests = [];
|
|
|
|
if (!empty($content)) {
|
|
$decoded = json_decode($content, true);
|
|
if (is_array($decoded)) {
|
|
// Keep only requests within the time window
|
|
$requests = array_values(
|
|
array_filter($decoded, fn($ts) => $ts > ($now - $timeWindow))
|
|
);
|
|
}
|
|
}
|
|
|
|
if (count($requests) >= $maxRequests) {
|
|
flock($fp, LOCK_UN);
|
|
fclose($fp);
|
|
|
|
header('Retry-After: ' . $timeWindow);
|
|
json_error('Too Many Requests. Please slow down.', 429);
|
|
}
|
|
|
|
// Record this request
|
|
$requests[] = $now;
|
|
|
|
// Write updated data back
|
|
ftruncate($fp, 0);
|
|
rewind($fp);
|
|
fwrite($fp, json_encode($requests));
|
|
|
|
} finally {
|
|
flock($fp, LOCK_UN);
|
|
fclose($fp);
|
|
}
|
|
}
|
|
}
|