Compare commits

...

3 Commits

9 changed files with 217 additions and 4 deletions

View File

@ -8,8 +8,19 @@ class RateLimiter {
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
public $autoBlacklistDuration = 24; // Hours to blacklist for
public $authRatelimitTable = 'login_attempts';
public $pagesRatelimitTable = 'pages_rate_limits';
public $whitelistTable = 'ip_whitelist';
public $blacklistTable = 'ip_blacklist';
private $pageLimits = [
// Default rate limits per minute
'default' => 60,
'admin' => 120,
'message' => 20,
'contact' => 30,
'call' => 30,
'register' => 5,
'config' => 10
];
public function __construct($database) {
$this->db = $database->getConnection();
@ -19,7 +30,7 @@ class RateLimiter {
// Database preparation
private function createTablesIfNotExist() {
// Login attempts table
// Authentication attempts table
$sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL UNIQUE,
@ -28,6 +39,15 @@ class RateLimiter {
)";
$this->db->exec($sql);
// Pages rate limits table
$sql = "CREATE TABLE IF NOT EXISTS {$this->pagesRatelimitTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
endpoint TEXT NOT NULL,
request_time DATETIME DEFAULT CURRENT_TIMESTAMP
)";
$this->db->exec($sql);
// IP whitelist table
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -454,4 +474,121 @@ class RateLimiter {
public function getDecayMinutes() {
return $this->decayMinutes;
}
/**
* Check if a page request is allowed
*/
public function isPageRequestAllowed($ipAddress, $endpoint, $userId = null) {
// First check if IP is blacklisted
if ($this->isIpBlacklisted($ipAddress)) {
return false;
}
// Then check if IP is whitelisted
if ($this->isIpWhitelisted($ipAddress)) {
return true;
}
// Clean old requests
$this->cleanOldPageRequests();
// Get limit based on endpoint type and user role
$limit = $this->getPageLimitForEndpoint($endpoint, $userId);
// Count recent requests
$sql = "SELECT COUNT(*) as request_count
FROM {$this->pagesRatelimitTable}
WHERE ip_address = :ip
AND endpoint = :endpoint
AND request_time > DATETIME('now', '-1 minute')";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':endpoint' => $endpoint
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result['request_count'] < $limit;
}
/**
* Record a page request
*/
public function recordPageRequest($ipAddress, $endpoint) {
$sql = "INSERT INTO {$this->pagesRatelimitTable} (ip_address, endpoint)
VALUES (:ip, :endpoint)";
$stmt = $this->db->prepare($sql);
return $stmt->execute([
':ip' => $ipAddress,
':endpoint' => $endpoint
]);
}
/**
* Clean old page requests
*/
private function cleanOldPageRequests() {
$sql = "DELETE FROM {$this->pagesRatelimitTable}
WHERE request_time < DATETIME('now', '-1 minute')";
$stmt = $this->db->prepare($sql);
$stmt->execute();
}
/**
* Get page rate limit for endpoint
*/
private function getPageLimitForEndpoint($endpoint, $userId = null) {
// Admin users get higher limits
if ($userId) {
$userObj = new User($this->db);
if ($userObj->hasRight($userId, 'admin')) {
return $this->pageLimits['admin'];
}
}
// Get endpoint type from the endpoint path
$endpointType = $this->getEndpointType($endpoint);
// Return specific limit if exists, otherwise default
return isset($this->pageLimits[$endpointType])
? $this->pageLimits[$endpointType]
: $this->pageLimits['default'];
}
/**
* Get endpoint type from path
*/
private function getEndpointType($endpoint) {
if (strpos($endpoint, 'message') !== false) return 'message';
if (strpos($endpoint, 'contact') !== false) return 'contact';
if (strpos($endpoint, 'call') !== false) return 'call';
if (strpos($endpoint, 'register') !== false) return 'register';
if (strpos($endpoint, 'config') !== false) return 'config';
return 'default';
}
/**
* Get remaining page requests
*/
public function getRemainingPageRequests($ipAddress, $endpoint, $userId = null) {
$limit = $this->getPageLimitForEndpoint($endpoint, $userId);
$sql = "SELECT COUNT(*) as request_count
FROM {$this->pagesRatelimitTable}
WHERE ip_address = :ip
AND endpoint = :endpoint
AND request_time > DATETIME('now', '-1 minute')";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':ip' => $ipAddress,
':endpoint' => $endpoint
]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return max(0, $limit - $result['request_count']);
}
}

View File

@ -0,0 +1,49 @@
<?php
require_once __DIR__ . '/../classes/ratelimiter.php';
/**
* Rate limit middleware for page requests
*
* @param Database $database Database connection
* @param string $endpoint The endpoint being accessed
* @param int|null $userId Current user ID if authenticated
* @return bool True if request is allowed, false if rate limited
*/
function checkRateLimit($database, $endpoint, $userId = null) {
$rateLimiter = new RateLimiter($database);
$ipAddress = $_SERVER['REMOTE_ADDR'];
// Check if request is allowed
if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) {
// Get remaining requests for error message
$remaining = $rateLimiter->getRemainingPageRequests($ipAddress, $endpoint, $userId);
// Set rate limit headers
header('X-RateLimit-Remaining: ' . $remaining);
header('X-RateLimit-Reset: ' . (time() + 60)); // Reset in 1 minute
// Return 429 Too Many Requests
http_response_code(429);
// If AJAX request, return JSON
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Too many requests. Please try again in a minute.',
'messageData' => Feedback::getMessageData('ERROR', 'DEFAULT', 'Too many requests. Please try again in a minute.', true)
]);
} else {
// For regular requests, set flash message and redirect
Feedback::flash('ERROR', 'DEFAULT', 'Too many requests. Please try again in a minute.', true);
header('Location: ' . htmlspecialchars($app_root));
}
exit;
}
// Record this request
$rateLimiter->recordPageRequest($ipAddress, $endpoint);
return true;
}

View File

@ -47,6 +47,11 @@ function isCacheExpired($agentId) {
// Handle POST request (saving to cache)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Apply rate limiting for adding new contacts
require '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'contact', $user_id);
// Validate agent ID for POST operations
if ($agentId === false || $agentId === null) {
Feedback::flash('ERROR', 'DEFAULT', 'Invalid agent ID format');

View File

@ -11,9 +11,10 @@ include '../app/includes/feedback-get.php';
include '../app/includes/feedback-show.php';
require '../app/classes/config.php';
$configObject = new Config();
require '../app/includes/rate_limit_middleware.php';
// For AJAX requests
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
@ -26,6 +27,9 @@ if (!$isWritable) {
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Apply rate limiting
checkRateLimit($dbWeb, 'config', $user_id);
// Ensure no output before this point
ob_clean();

View File

@ -27,6 +27,10 @@ try {
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
try {
// apply page rate limiting
require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'login', null); // null since user is not logged in yet
// Validate form data
$security = SecurityHelper::getInstance();
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'remember_me', 'csrf_token']);

View File

@ -21,7 +21,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Apply rate limiting for profile operations
require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($db, 'profile', $user_id);
checkRateLimit($dbWeb, 'profile', $user_id);
$item = $_REQUEST['item'] ?? '';

View File

@ -17,6 +17,11 @@ if ($config['registration_enabled'] == true) {
$dbWeb = connectDB($config);
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
// Apply rate limiting
require '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'register');
require_once '../app/classes/validator.php';
$validator = new Validator($_POST);

View File

@ -24,6 +24,11 @@ $rateLimiter = new RateLimiter($dbWeb);
// Handle form submissions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
require_once '../app/classes/validator.php';
// Apply rate limiting for security operations
require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'security', $user_id);
$action = $_POST['action'];
$validator = new Validator($_POST);
@ -147,7 +152,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
// Always show rate limit info message for rate limiting section
if ($section === 'ratelimit') {
$messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO'];
$system_messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO'];
}
// Get current lists

View File

@ -26,6 +26,10 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
* Handles form submissions from editing
*/
// Apply rate limiting for profile operations
require_once '../app/includes/rate_limit_middleware.php';
checkRateLimit($dbWeb, 'profile', $user_id);
// Get hash from URL if present
$hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? '';
$redirectUrl = htmlspecialchars($app_root) . '?page=settings';