Compare commits
3 Commits
00e2a38087
...
144dd6e742
Author | SHA1 | Date |
---|---|---|
|
144dd6e742 | |
|
c465fbfdf4 | |
|
beafdf29fb |
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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'] ?? '';
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue