Security Hardening: Phase 1-3 complete

- 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)
This commit is contained in:
Hamza-Ayed
2026-05-03 21:06:17 +03:00
parent b33513ebcf
commit 214d96ee8d
11 changed files with 236 additions and 130 deletions

View File

@@ -1,6 +1,6 @@
<?php
/**
* Simple Rate Limiting Middleware
* Rate Limiting Middleware (File-based, Race-Condition Safe)
*/
declare(strict_types=1);
@@ -10,44 +10,62 @@ namespace App\Middleware;
final class RateLimitMiddleware
{
/**
* Basic file-based rate limiter to keep dependencies zero.
* In a production multi-server setup, switch this to Redis/DB.
* 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 . '/rate_limit_' . md5($ip) . '.json';
// Ensure cache directory exists
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$cacheDir = STORAGE_PATH . '/cache';
$cacheFile = $cacheDir . '/rl_' . md5($ip) . '.json';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
$now = time();
$requests = [];
// Read existing requests if file exists and is writable
if (file_exists($cacheFile)) {
$content = file_get_contents($cacheFile);
if ($content !== false) {
$data = json_decode($content, true);
if (is_array($data)) {
// Filter out requests older than the time window
$requests = array_filter($data, fn($timestamp) => $timestamp > ($now - $timeWindow));
// 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);
}
// Check limit
if (count($requests) >= $maxRequests) {
json_error('Too Many Requests. Please try again later.', 429);
}
// Add current request
$requests[] = $now;
// Save back to file
file_put_contents($cacheFile, json_encode(array_values($requests)));
}
}