Compare commits
No commits in common. "144dd6e742c53710fb62d8bb7e5bcc403c8618f9" and "00e2a380871835981adf2c13015cf86fe699cce9" have entirely different histories.
144dd6e742
...
00e2a38087
|
@ -8,19 +8,8 @@ class RateLimiter {
|
||||||
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
|
public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
|
||||||
public $autoBlacklistDuration = 24; // Hours to blacklist for
|
public $autoBlacklistDuration = 24; // Hours to blacklist for
|
||||||
public $authRatelimitTable = 'login_attempts';
|
public $authRatelimitTable = 'login_attempts';
|
||||||
public $pagesRatelimitTable = 'pages_rate_limits';
|
|
||||||
public $whitelistTable = 'ip_whitelist';
|
public $whitelistTable = 'ip_whitelist';
|
||||||
public $blacklistTable = 'ip_blacklist';
|
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) {
|
public function __construct($database) {
|
||||||
$this->db = $database->getConnection();
|
$this->db = $database->getConnection();
|
||||||
|
@ -30,7 +19,7 @@ class RateLimiter {
|
||||||
|
|
||||||
// Database preparation
|
// Database preparation
|
||||||
private function createTablesIfNotExist() {
|
private function createTablesIfNotExist() {
|
||||||
// Authentication attempts table
|
// Login attempts table
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
ip_address TEXT NOT NULL UNIQUE,
|
ip_address TEXT NOT NULL UNIQUE,
|
||||||
|
@ -39,15 +28,6 @@ class RateLimiter {
|
||||||
)";
|
)";
|
||||||
$this->db->exec($sql);
|
$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
|
// IP whitelist table
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
@ -474,121 +454,4 @@ class RateLimiter {
|
||||||
public function getDecayMinutes() {
|
public function getDecayMinutes() {
|
||||||
return $this->decayMinutes;
|
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']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
<?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,11 +47,6 @@ function isCacheExpired($agentId) {
|
||||||
|
|
||||||
// Handle POST request (saving to cache)
|
// Handle POST request (saving to cache)
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
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
|
// Validate agent ID for POST operations
|
||||||
if ($agentId === false || $agentId === null) {
|
if ($agentId === false || $agentId === null) {
|
||||||
Feedback::flash('ERROR', 'DEFAULT', 'Invalid agent ID format');
|
Feedback::flash('ERROR', 'DEFAULT', 'Invalid agent ID format');
|
||||||
|
|
|
@ -11,9 +11,8 @@ include '../app/includes/feedback-get.php';
|
||||||
include '../app/includes/feedback-show.php';
|
include '../app/includes/feedback-show.php';
|
||||||
|
|
||||||
require '../app/classes/config.php';
|
require '../app/classes/config.php';
|
||||||
$configObject = new Config();
|
|
||||||
|
|
||||||
require '../app/includes/rate_limit_middleware.php';
|
$configObject = new Config();
|
||||||
|
|
||||||
// For AJAX requests
|
// For AJAX requests
|
||||||
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||||
|
@ -27,9 +26,6 @@ if (!$isWritable) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// Apply rate limiting
|
|
||||||
checkRateLimit($dbWeb, 'config', $user_id);
|
|
||||||
|
|
||||||
// Ensure no output before this point
|
// Ensure no output before this point
|
||||||
ob_clean();
|
ob_clean();
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,6 @@ try {
|
||||||
|
|
||||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
||||||
try {
|
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
|
// Validate form data
|
||||||
$security = SecurityHelper::getInstance();
|
$security = SecurityHelper::getInstance();
|
||||||
$formData = $security->sanitizeArray($_POST, ['username', 'password', 'remember_me', 'csrf_token']);
|
$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
|
// Apply rate limiting for profile operations
|
||||||
require_once '../app/includes/rate_limit_middleware.php';
|
require_once '../app/includes/rate_limit_middleware.php';
|
||||||
checkRateLimit($dbWeb, 'profile', $user_id);
|
checkRateLimit($db, 'profile', $user_id);
|
||||||
|
|
||||||
$item = $_REQUEST['item'] ?? '';
|
$item = $_REQUEST['item'] ?? '';
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,6 @@ if ($config['registration_enabled'] == true) {
|
||||||
$dbWeb = connectDB($config);
|
$dbWeb = connectDB($config);
|
||||||
|
|
||||||
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
|
||||||
|
|
||||||
// Apply rate limiting
|
|
||||||
require '../app/includes/rate_limit_middleware.php';
|
|
||||||
checkRateLimit($dbWeb, 'register');
|
|
||||||
|
|
||||||
require_once '../app/classes/validator.php';
|
require_once '../app/classes/validator.php';
|
||||||
|
|
||||||
$validator = new Validator($_POST);
|
$validator = new Validator($_POST);
|
||||||
|
|
|
@ -24,11 +24,6 @@ $rateLimiter = new RateLimiter($dbWeb);
|
||||||
// Handle form submissions
|
// Handle form submissions
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
require_once '../app/classes/validator.php';
|
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'];
|
$action = $_POST['action'];
|
||||||
$validator = new Validator($_POST);
|
$validator = new Validator($_POST);
|
||||||
|
|
||||||
|
@ -152,7 +147,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
|
|
||||||
// Always show rate limit info message for rate limiting section
|
// Always show rate limit info message for rate limiting section
|
||||||
if ($section === 'ratelimit') {
|
if ($section === 'ratelimit') {
|
||||||
$system_messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO'];
|
$messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current lists
|
// Get current lists
|
||||||
|
|
|
@ -26,10 +26,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
* Handles form submissions from editing
|
* 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
|
// Get hash from URL if present
|
||||||
$hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? '';
|
$hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? '';
|
||||||
$redirectUrl = htmlspecialchars($app_root) . '?page=settings';
|
$redirectUrl = htmlspecialchars($app_root) . '?page=settings';
|
||||||
|
|
Loading…
Reference in New Issue