add admin cookie check
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BotLimiter - Smart Traffic Control
|
* BotLimiter - Smart Traffic Control
|
||||||
* @author Panariga
|
* @author Panariga
|
||||||
@@ -8,9 +9,18 @@ if (!defined('_PS_VERSION_')) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once dirname(__FILE__) . '/classes/RuleManager.php';
|
spl_autoload_register(function ($className) {
|
||||||
require_once dirname(__FILE__) . '/classes/BotLogger.php';
|
$classFile = _PS_MODULE_DIR_ . 'botlimiter/classes/' . $className . '.php';
|
||||||
|
if (file_exists($classFile)) {
|
||||||
|
require_once $classFile;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
spl_autoload_register(function ($className) {
|
||||||
|
$classFile = _PS_MODULE_DIR_ . 'botlimiter/classes/rules' . $className . '.php';
|
||||||
|
if (file_exists($classFile)) {
|
||||||
|
require_once $classFile;
|
||||||
|
}
|
||||||
|
});
|
||||||
class BotLimiter extends Module
|
class BotLimiter extends Module
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
@@ -36,6 +46,9 @@ class BotLimiter extends Module
|
|||||||
|
|
||||||
public function hookActionFrontControllerInitBefore($params)
|
public function hookActionFrontControllerInitBefore($params)
|
||||||
{
|
{
|
||||||
|
if (!empty((new Cookie('psAdmin'))->id_employee)) {
|
||||||
|
return; // Don't display the tag for logged-in administrators
|
||||||
|
}
|
||||||
// 1. Skip if we are currently ON the verification page to avoid loops
|
// 1. Skip if we are currently ON the verification page to avoid loops
|
||||||
if ($this->context->controller instanceof BotLimiterVerifyModuleFrontController) {
|
if ($this->context->controller instanceof BotLimiterVerifyModuleFrontController) {
|
||||||
return;
|
return;
|
||||||
@@ -47,7 +60,8 @@ class BotLimiter extends Module
|
|||||||
// 3. Register Rules (Add more here in future)
|
// 3. Register Rules (Add more here in future)
|
||||||
$manager->addRule(new HeadRequestRule());
|
$manager->addRule(new HeadRequestRule());
|
||||||
$manager->addRule(new FilterTrapRule());
|
$manager->addRule(new FilterTrapRule());
|
||||||
|
$manager->addRule(new ScanBotsRule());
|
||||||
|
$manager->addRule(new RateLimitRule());
|
||||||
// 4. Execute Rules
|
// 4. Execute Rules
|
||||||
// If a rule returns FALSE, it means traffic is blocked or redirected.
|
// If a rule returns FALSE, it means traffic is blocked or redirected.
|
||||||
// The rule is responsible for the redirect/exit.
|
// The rule is responsible for the redirect/exit.
|
||||||
|
|||||||
@@ -3,10 +3,20 @@
|
|||||||
class BotLogger
|
class BotLogger
|
||||||
{
|
{
|
||||||
const LOG_FILE = _PS_ROOT_DIR_ . '/var/logs/botlimiter_ban.log';
|
const LOG_FILE = _PS_ROOT_DIR_ . '/var/logs/botlimiter_ban.log';
|
||||||
|
const WHITELIST_FILE = _PS_CACHE_DIR_ . DIRECTORY_SEPARATOR . 'botlimiter_whitelist.php';
|
||||||
const MAX_SIZE = 10485760; // 10 MB
|
const MAX_SIZE = 10485760; // 10 MB
|
||||||
|
const UPDATE_INTERVAL = 86400; // 24 hours (in seconds)
|
||||||
|
|
||||||
|
// Store in memory during execution so we don't read the file multiple times per request
|
||||||
|
private static $cachedWhitelist = null;
|
||||||
|
|
||||||
public static function logBan($ip, $reason)
|
public static function logBan($ip, $reason)
|
||||||
{
|
{
|
||||||
|
// 0. Check if IP is a known good bot (Google, Bing, etc.)
|
||||||
|
if (self::isWhitelisted($ip)) {
|
||||||
|
return; // Exit silently, do not log or ban
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Check file size before writing (The Safety Valve)
|
// 1. Check file size before writing (The Safety Valve)
|
||||||
if (file_exists(self::LOG_FILE) && filesize(self::LOG_FILE) > self::MAX_SIZE) {
|
if (file_exists(self::LOG_FILE) && filesize(self::LOG_FILE) > self::MAX_SIZE) {
|
||||||
self::rotateLog();
|
self::rotateLog();
|
||||||
@@ -19,6 +29,173 @@ class BotLogger
|
|||||||
file_put_contents(self::LOG_FILE, $message, FILE_APPEND | LOCK_EX);
|
file_put_contents(self::LOG_FILE, $message, FILE_APPEND | LOCK_EX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given IP belongs to the whitelisted networks.
|
||||||
|
*/
|
||||||
|
public static function isWhitelisted($ip)
|
||||||
|
{
|
||||||
|
|
||||||
|
// 1. FASTEST CHECK: Is it the server itself? (Localhost or Server IP)
|
||||||
|
$serverIps = ['127.0.0.1', '::1'];
|
||||||
|
if (!empty($_SERVER['SERVER_ADDR'])) {
|
||||||
|
$serverIps[] = $_SERVER['SERVER_ADDR'];
|
||||||
|
}
|
||||||
|
if (in_array($ip, $serverIps, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. FAST CHECK: PrestaShop Maintenance IPs
|
||||||
|
$maintenance_ips = Configuration::get('PS_MAINTENANCE_IP');
|
||||||
|
if ($maintenance_ips) {
|
||||||
|
$admin_ips = array_filter(array_map('trim', explode(',', $maintenance_ips)));
|
||||||
|
if (in_array($ip, $admin_ips, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self::updateWhitelistIfNeeded();
|
||||||
|
|
||||||
|
// Load whitelist into memory if not already done
|
||||||
|
if (self::$cachedWhitelist === null) {
|
||||||
|
if (file_exists(self::WHITELIST_FILE)) {
|
||||||
|
// FASTEST: OPcache will serve this directly from RAM
|
||||||
|
self::$cachedWhitelist = include(self::WHITELIST_FILE);
|
||||||
|
} else {
|
||||||
|
self::$cachedWhitelist = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against CIDR blocks
|
||||||
|
foreach (self::$cachedWhitelist as $cidr) {
|
||||||
|
if (self::ipMatch($ip, $cidr)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the whitelist once a day by fetching JSON lists from Google and Bing.
|
||||||
|
*/
|
||||||
|
private static function updateWhitelistIfNeeded()
|
||||||
|
{
|
||||||
|
$needsUpdate = true;
|
||||||
|
|
||||||
|
if (file_exists(self::WHITELIST_FILE)) {
|
||||||
|
$lastModified = filemtime(self::WHITELIST_FILE);
|
||||||
|
if ((time() - $lastModified) < self::UPDATE_INTERVAL) {
|
||||||
|
$needsUpdate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$needsUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$cidrs = [];
|
||||||
|
|
||||||
|
|
||||||
|
$urls = [
|
||||||
|
'https://developers.google.com/search/apis/ipranges/googlebot.json',
|
||||||
|
'https://www.bing.com/toolbox/bingbot.json',
|
||||||
|
'https://openai.com/chatgpt-user.json',
|
||||||
|
'https://openai.com/searchbot.json',
|
||||||
|
'https://openai.com/gptbot.json'
|
||||||
|
|
||||||
|
];
|
||||||
|
foreach ($urls as $url) {
|
||||||
|
$cidrs = array_merge($cidrs, self::extractIpPrefix($url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we successfully fetched networks, update the file
|
||||||
|
if (!empty($cidrs)) {
|
||||||
|
$cidrs = array_values(array_unique($cidrs));
|
||||||
|
|
||||||
|
// Generate valid PHP code
|
||||||
|
$phpCode = "<?php\n\n// Auto-generated whitelist\nreturn " . var_export($cidrs, true) . ";\n";
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
file_put_contents(self::WHITELIST_FILE, $phpCode, LOCK_EX);
|
||||||
|
|
||||||
|
// IMPORTANT: Clear the old file from OPcache so PHP knows it changed!
|
||||||
|
if (function_exists('opcache_invalidate')) {
|
||||||
|
opcache_invalidate(self::WHITELIST_FILE, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$cachedWhitelist = $cidrs;
|
||||||
|
} else {
|
||||||
|
// If fetching failed (network issue), touch the file to delay the next retry to prevent spamming
|
||||||
|
if (file_exists(self::WHITELIST_FILE)) {
|
||||||
|
touch(self::WHITELIST_FILE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function extractIpPrefix(string $url): array
|
||||||
|
{
|
||||||
|
$cidrs = [];
|
||||||
|
$json = @Tools::file_get_contents($url);
|
||||||
|
if ($json) {
|
||||||
|
$data = json_decode($json, true);
|
||||||
|
if (isset($data['prefixes'])) {
|
||||||
|
foreach ($data['prefixes'] as $prefix) {
|
||||||
|
if (isset($prefix['ipv4Prefix'])) $cidrs[] = $prefix['ipv4Prefix'];
|
||||||
|
if (isset($prefix['ipv6Prefix'])) $cidrs[] = $prefix['ipv6Prefix'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $cidrs;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Matches an IPv4 or IPv6 address against a CIDR block.
|
||||||
|
*/
|
||||||
|
private static function ipMatch($ip, $cidr)
|
||||||
|
{
|
||||||
|
if (strpos($cidr, '/') === false) {
|
||||||
|
return $ip === $cidr;
|
||||||
|
}
|
||||||
|
|
||||||
|
list($subnet, $mask) = explode('/', $cidr);
|
||||||
|
|
||||||
|
// Match IPv4
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
$ip_long = ip2long($ip);
|
||||||
|
$subnet_long = ip2long($subnet);
|
||||||
|
$mask_long = ~((1 << (32 - $mask)) - 1);
|
||||||
|
return ($ip_long & $mask_long) === ($subnet_long & $mask_long);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match IPv6
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
$ip_bin = inet_pton($ip);
|
||||||
|
$subnet_bin = inet_pton($subnet);
|
||||||
|
|
||||||
|
if (!$ip_bin || !$subnet_bin) return false;
|
||||||
|
|
||||||
|
$bytes = floor($mask / 8);
|
||||||
|
$bits = $mask % 8;
|
||||||
|
|
||||||
|
if ($bytes > 0 && substr($ip_bin, 0, $bytes) !== substr($subnet_bin, 0, $bytes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bits > 0) {
|
||||||
|
$ip_byte = ord($ip_bin[$bytes]);
|
||||||
|
$subnet_byte = ord($subnet_bin[$bytes]);
|
||||||
|
$bitmask = ~((1 << (8 - $bits)) - 1) & 0xFF;
|
||||||
|
if (($ip_byte & $bitmask) !== ($subnet_byte & $bitmask)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rotates the log:
|
* Rotates the log:
|
||||||
* 1. Deletes the .old file
|
* 1. Deletes the .old file
|
||||||
@@ -37,4 +214,76 @@ class BotLogger
|
|||||||
// Rename current to backup
|
// Rename current to backup
|
||||||
@rename(self::LOG_FILE, $backup_file);
|
@rename(self::LOG_FILE, $backup_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely gets the real client IP, completely immune to Header Spoofing.
|
||||||
|
*/
|
||||||
|
public static function getRealIp()
|
||||||
|
{
|
||||||
|
// 1. Get the actual, un-spoofable TCP connection IP
|
||||||
|
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||||
|
|
||||||
|
// 2. Is the connection physically coming from Cloudflare?
|
||||||
|
if (self::isCloudflareIp($remoteAddr)) {
|
||||||
|
|
||||||
|
// ONLY trust these headers because Cloudflare guaranteed them
|
||||||
|
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP']) && filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP)) {
|
||||||
|
return $_SERVER['HTTP_CF_CONNECTING_IP'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||||
|
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||||
|
$ip = trim($ips[0]);
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback: If it's NOT Cloudflare, it's either a direct user or a hacker.
|
||||||
|
// We MUST ignore their headers and use the raw TCP connection IP.
|
||||||
|
return $remoteAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the IP belongs to official Cloudflare Networks.
|
||||||
|
* (These ranges rarely change, standard practice is to hardcode them)
|
||||||
|
*/
|
||||||
|
private static function isCloudflareIp($ip)
|
||||||
|
{
|
||||||
|
static $cf_ips = [
|
||||||
|
// IPv4
|
||||||
|
'173.245.48.0/20',
|
||||||
|
'103.21.244.0/22',
|
||||||
|
'103.22.200.0/22',
|
||||||
|
'103.31.4.0/22',
|
||||||
|
'141.101.64.0/18',
|
||||||
|
'108.162.192.0/18',
|
||||||
|
'190.93.240.0/20',
|
||||||
|
'188.114.96.0/20',
|
||||||
|
'197.234.240.0/22',
|
||||||
|
'198.41.128.0/17',
|
||||||
|
'162.158.0.0/15',
|
||||||
|
'104.16.0.0/13',
|
||||||
|
'104.24.0.0/14',
|
||||||
|
'172.64.0.0/13',
|
||||||
|
'131.0.72.0/22',
|
||||||
|
// IPv6
|
||||||
|
'2400:cb00::/32',
|
||||||
|
'2606:4700::/32',
|
||||||
|
'2803:f800::/32',
|
||||||
|
'2405:b500::/32',
|
||||||
|
'2405:8100::/32',
|
||||||
|
'2a06:98c0::/29',
|
||||||
|
'2c0f:f248::/32'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($cf_ips as $cidr) {
|
||||||
|
if (self::ipMatch($ip, $cidr)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
require_once dirname(__FILE__) . '/rules/RuleInterface.php';
|
require_once dirname(__FILE__) . '/rules/RuleInterface.php';
|
||||||
require_once dirname(__FILE__) . '/rules/HeadRequestRule.php';
|
require_once dirname(__FILE__) . '/rules/HeadRequestRule.php';
|
||||||
require_once dirname(__FILE__) . '/rules/FilterTrapRule.php';
|
require_once dirname(__FILE__) . '/rules/FilterTrapRule.php';
|
||||||
|
require_once dirname(__FILE__) . '/rules/ScanBotsRule.php';
|
||||||
|
require_once dirname(__FILE__) . '/rules/RateLimitRule.php';
|
||||||
|
|
||||||
|
|
||||||
class RuleManager
|
class RuleManager
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ class FilterTrapRule implements RuleInterface
|
|||||||
{
|
{
|
||||||
public function execute()
|
public function execute()
|
||||||
{
|
{
|
||||||
|
$ip = BotLogger::getRealIp();
|
||||||
|
if (BotLogger::isWhitelisted($ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// 1. Only analyze heavy requests (filters/sorting)
|
// 1. Only analyze heavy requests (filters/sorting)
|
||||||
if (!Tools::getIsset('q') && !Tools::getIsset('order')) {
|
if (!Tools::getIsset('q') && !Tools::getIsset('order')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$context = Context::getContext();
|
$context = Context::getContext();
|
||||||
$ip = $_SERVER['REMOTE_ADDR'];
|
|
||||||
|
|
||||||
// 2. Allow whitelisted Bots (Google/Bing)
|
// 2. Allow whitelisted Bots (Google/Bing)
|
||||||
// We trust them not to spam. If they do, use robots.txt.
|
// We trust them not to spam. If they do, use robots.txt.
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ class HeadRequestRule implements RuleInterface
|
|||||||
{
|
{
|
||||||
public function execute()
|
public function execute()
|
||||||
{
|
{
|
||||||
|
$ip = BotLogger::getRealIp();
|
||||||
|
if (BotLogger::isWhitelisted($ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// Detect HEAD request with Filter parameters
|
// Detect HEAD request with Filter parameters
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'HEAD' && (Tools::getIsset('q') || Tools::getIsset('order'))) {
|
if ($_SERVER['REQUEST_METHOD'] === 'HEAD' && (Tools::getIsset('q') || Tools::getIsset('order'))) {
|
||||||
|
|
||||||
// Log for Fail2Ban
|
// Log for Fail2Ban
|
||||||
BotLogger::logBan($_SERVER['REMOTE_ADDR'], 'HEAD_REQUEST_SPAM');
|
BotLogger::logBan($ip, 'HEAD_REQUEST_SPAM');
|
||||||
|
|
||||||
header('HTTP/1.1 405 Method Not Allowed');
|
header('HTTP/1.1 405 Method Not Allowed');
|
||||||
die('Method Not Allowed');
|
die('Method Not Allowed');
|
||||||
|
|||||||
68
classes/rules/RateLimitRule.php
Normal file
68
classes/rules/RateLimitRule.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitRule implements RuleInterface
|
||||||
|
{
|
||||||
|
const MAX_404_REQUESTS = 20; // Allow a maximum of 20 dead links...
|
||||||
|
const TIME_WINDOW = 300; // ...within 300 seconds (5 minutes)
|
||||||
|
|
||||||
|
public function execute()
|
||||||
|
{
|
||||||
|
$ip = BotLogger::getRealIp();
|
||||||
|
if (BotLogger::isWhitelisted($ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$context = Context::getContext();
|
||||||
|
|
||||||
|
// 1. Instantly skip if this is NOT a 404 error page.
|
||||||
|
// During the hookActionFrontControllerInitBefore hook, PrestaShop has already
|
||||||
|
// resolved the route. If it failed, the controller is set to PageNotFoundController.
|
||||||
|
if (!($context->controller instanceof PageNotFoundController)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 2. Safely check if the Cache module is installed and enabled
|
||||||
|
if (!Module::isInstalled('dbmemorycache') || !Module::isEnabled('dbmemorycache')) {
|
||||||
|
return true; // Pass gracefully if the module is missing or disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var DbMemoryCache $cache */
|
||||||
|
$cache = Module::getInstanceByName('dbmemorycache');
|
||||||
|
if (!$cache) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Generate a unique hash for this specific IP's 404 traffic
|
||||||
|
$cacheKey = hash('sha256', '404_spam_limiter_' . $ip);
|
||||||
|
|
||||||
|
// 4. Lookup their current 404 hit count in the memory table
|
||||||
|
$currentCount = 0;
|
||||||
|
if ($cache->existsValue($cacheKey)) {
|
||||||
|
$currentCount = (int) $cache->getValue($cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Increment their strike counter
|
||||||
|
$currentCount++;
|
||||||
|
|
||||||
|
// 6. Have they triggered too many 404s?
|
||||||
|
if ($currentCount > self::MAX_404_REQUESTS) {
|
||||||
|
|
||||||
|
// Log it so Fail2ban can block them at the server firewall level
|
||||||
|
BotLogger::logBan($ip, '404_RATE_LIMIT_EXCEEDED');
|
||||||
|
|
||||||
|
// Drop the connection instantly to save server resources
|
||||||
|
header('HTTP/1.1 429 Too Many Requests');
|
||||||
|
die('429 Too Many Requests - Stop Scanning');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Save the new strike count back to the cache
|
||||||
|
// Note: Because we overwrite it here, this creates a "Sliding Window".
|
||||||
|
// If a bot keeps spamming, the 300s timer resets every time, keeping them trapped!
|
||||||
|
$cache->setValue($cacheKey, $currentCount, self::TIME_WINDOW);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
200
classes/rules/ScanBotsRule.php
Normal file
200
classes/rules/ScanBotsRule.php
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class ScanBotsRule implements RuleInterface
|
||||||
|
{
|
||||||
|
// 1. Exact matches using Keys for O(1) Instant Lookup
|
||||||
|
// Redundant wp-content/ and wp-includes/ files were removed because the prefix scanner catches them all!
|
||||||
|
private static $exact_targets = [
|
||||||
|
'/database.php' => true,
|
||||||
|
'/acp.php' => true,
|
||||||
|
'/gettest.php' => true,
|
||||||
|
'/4h.php' => true,
|
||||||
|
'/flower.php' => true,
|
||||||
|
'/styll.php' => true,
|
||||||
|
'/re.php' => true,
|
||||||
|
'/alfashell.php' => true,
|
||||||
|
'/axx.php' => true,
|
||||||
|
'/X57.php' => true,
|
||||||
|
'/erty.php' => true,
|
||||||
|
'/miansha.php' => true,
|
||||||
|
'/bengi.php' => true,
|
||||||
|
'/bs1.php' => true,
|
||||||
|
'/motu.php' => true,
|
||||||
|
'/gssdd.php' => true,
|
||||||
|
'/in.php' => true,
|
||||||
|
'/bal.php' => true,
|
||||||
|
'/dev.php' => true,
|
||||||
|
'/k.php' => true,
|
||||||
|
'/prv8.php' => true,
|
||||||
|
'/lb.php' => true,
|
||||||
|
'/hi.php' => true,
|
||||||
|
'/f35.php' => true,
|
||||||
|
'/update/f35.php' => true,
|
||||||
|
'/a1.php' => true,
|
||||||
|
'/fi.php' => true,
|
||||||
|
'/init.php' => true,
|
||||||
|
'/abcd.php' => true,
|
||||||
|
'/av.php' => true,
|
||||||
|
'/kj.php' => true,
|
||||||
|
'/fe5.php' => true,
|
||||||
|
'/about.php' => true,
|
||||||
|
'/ok.php' => true,
|
||||||
|
'/w4.php' => true,
|
||||||
|
'/assets/css/index.php' => true,
|
||||||
|
'/wp.php' => true,
|
||||||
|
'/BDKR28WP.php' => true,
|
||||||
|
'/wp-the.php' => true,
|
||||||
|
'/wp-michan.php' => true,
|
||||||
|
'/makeasmtp.php' => true,
|
||||||
|
'/alpha.php' => true,
|
||||||
|
'/we.php' => true,
|
||||||
|
'/155.php' => true,
|
||||||
|
'/goat.php' => true,
|
||||||
|
'/fff.php' => true,
|
||||||
|
'/ff1.php' => true,
|
||||||
|
'/cgi-bin/index.php' => true,
|
||||||
|
'/plugins.php' => true,
|
||||||
|
'/222.php' => true,
|
||||||
|
'/ms-edit.php' => true,
|
||||||
|
'/goods.php' => true,
|
||||||
|
'/adminfuns.php' => true,
|
||||||
|
'/166.php' => true,
|
||||||
|
'/test1.php' => true,
|
||||||
|
'/wp-blog.php' => true,
|
||||||
|
'/sbhu.php' => true,
|
||||||
|
'/wp-update.php' => true,
|
||||||
|
'/ms.php' => true,
|
||||||
|
'/x.php' => true,
|
||||||
|
'/tinyfilemanager.php' => true,
|
||||||
|
'/classwithtostring.php' => true,
|
||||||
|
'/aaa.php' => true,
|
||||||
|
'/plss3.php' => true,
|
||||||
|
'/06.php' => true,
|
||||||
|
'/a.php' => true,
|
||||||
|
'/xqq.php' => true,
|
||||||
|
'/class-t.api.php' => true,
|
||||||
|
'/wp-act.php' => true,
|
||||||
|
'/wp9.php' => true,
|
||||||
|
'/bless.php' => true,
|
||||||
|
'/file59.php' => true,
|
||||||
|
'/file.php' => true,
|
||||||
|
'/sc.php' => true,
|
||||||
|
'/1.php' => true,
|
||||||
|
'/aa.php' => true,
|
||||||
|
'/bgymj.php' => true,
|
||||||
|
'/style.php' => true,
|
||||||
|
'/inputs.php' => true,
|
||||||
|
'/f6.php' => true,
|
||||||
|
'/ol.php' => true,
|
||||||
|
'/xmlrpc.php' => true,
|
||||||
|
'/gifclass.php' => true,
|
||||||
|
'/66.php' => true,
|
||||||
|
'/ioxi-o.php' => true,
|
||||||
|
'/edit.php' => true,
|
||||||
|
'/3.php' => true,
|
||||||
|
'/wsvvs.php' => true,
|
||||||
|
'/pass2.php' => true,
|
||||||
|
'/maxro.php' => true,
|
||||||
|
'/mga.php' => true,
|
||||||
|
'/2.php' => true,
|
||||||
|
'/wdf.php' => true,
|
||||||
|
'/path.php' => true,
|
||||||
|
'/txets.php' => true,
|
||||||
|
'/sys.php' => true,
|
||||||
|
'/pp.php' => true,
|
||||||
|
'/g.php' => true,
|
||||||
|
'/h.php' => true,
|
||||||
|
'/xxxx.php' => true,
|
||||||
|
'/sty.php' => true,
|
||||||
|
'/a2.php' => true,
|
||||||
|
'/fvvff.php' => true,
|
||||||
|
'/claw.php' => true,
|
||||||
|
'/swallowable.php' => true,
|
||||||
|
'/foxr.php' => true,
|
||||||
|
'/w2025.php' => true,
|
||||||
|
'/cs.php' => true,
|
||||||
|
'/kk.php' => true,
|
||||||
|
'/rithin.php' => true,
|
||||||
|
'/h2h.php' => true,
|
||||||
|
'/wo.php' => true,
|
||||||
|
'/jocundly.php' => true,
|
||||||
|
'/rere.php' => true,
|
||||||
|
'/bafFz.php' => true,
|
||||||
|
'/elabel.php' => true,
|
||||||
|
'/teee.php' => true,
|
||||||
|
'/no1.php' => true,
|
||||||
|
'/akses.php' => true,
|
||||||
|
'/lp6.php' => true,
|
||||||
|
'/eee.php' => true,
|
||||||
|
'/asw.php' => true,
|
||||||
|
'/sf.php' => true,
|
||||||
|
'/by.php' => true,
|
||||||
|
'/x12.php' => true,
|
||||||
|
'/uuu.php' => true,
|
||||||
|
'/fsgdjkl.php' => true,
|
||||||
|
'/settings.php' => true,
|
||||||
|
'/utky.php' => true,
|
||||||
|
'/yos.php' => true,
|
||||||
|
'/albin.php' => true,
|
||||||
|
'/invisi.php' => true,
|
||||||
|
'/ty.php' => true,
|
||||||
|
'/wziar1.php' => true,
|
||||||
|
'/742.php' => true,
|
||||||
|
'/wp-p2r3q9c8k4.php' => true,
|
||||||
|
'/cash.php' => true,
|
||||||
|
'/nw_ok.php' => true,
|
||||||
|
'/filefuns.php' => true,
|
||||||
|
'/leon.php' => true,
|
||||||
|
'/199.php' => true,
|
||||||
|
'/aifa.php' => true,
|
||||||
|
'/gptsh.php' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 2. Prefix Targets (Folders/Directories)
|
||||||
|
// ANY traffic accessing these folders immediately triggers the ban.
|
||||||
|
private static $prefix_targets = [
|
||||||
|
'/wp-content/',
|
||||||
|
'/wp-includes/',
|
||||||
|
'/wp-admin/',
|
||||||
|
'/x/'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function execute()
|
||||||
|
{
|
||||||
|
if (empty($_SERVER['REQUEST_URI'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$ip = BotLogger::getRealIp();
|
||||||
|
|
||||||
|
if (BotLogger::isWhitelisted($ip)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 1. Strip Query Strings (e.g. ?id=1) so bots cannot bypass the exact match
|
||||||
|
$path = strtok($_SERVER['REQUEST_URI'], '?');
|
||||||
|
|
||||||
|
// 2. O(1) Instant RAM-speed check for exact files
|
||||||
|
if (isset(self::$exact_targets[$path])) {
|
||||||
|
$this->blockRequest($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Prefix Check for WordPress & Malicious Directories
|
||||||
|
foreach (self::$prefix_targets as $prefix) {
|
||||||
|
if (strpos($path, $prefix) === 0) {
|
||||||
|
$this->blockRequest($ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable trigger to log and drop connection
|
||||||
|
*/
|
||||||
|
private function blockRequest($ip)
|
||||||
|
{
|
||||||
|
BotLogger::logBan($ip, 'SCAN_BOT');
|
||||||
|
|
||||||
|
header('HTTP/1.1 405 Method Not Allowed');
|
||||||
|
die('Method Not Allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
11
config_uk.xml
Normal file
11
config_uk.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<module>
|
||||||
|
<name>botlimiter</name>
|
||||||
|
<displayName><![CDATA[Bot Limiter & Firewall]]></displayName>
|
||||||
|
<version><![CDATA[1.0.0]]></version>
|
||||||
|
<description><![CDATA[Intelligent protection against faceted search scrapers.]]></description>
|
||||||
|
<author><![CDATA[Panariga]]></author>
|
||||||
|
<tab><![CDATA[administration]]></tab>
|
||||||
|
<is_configurable>0</is_configurable>
|
||||||
|
<need_instance>0</need_instance>
|
||||||
|
</module>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once dirname(__FILE__) . '/../../classes/BotLogger.php';
|
||||||
|
|
||||||
class BotLimiterVerifyModuleFrontController extends ModuleFrontController
|
class BotLimiterVerifyModuleFrontController extends ModuleFrontController
|
||||||
{
|
{
|
||||||
@@ -7,7 +8,7 @@ class BotLimiterVerifyModuleFrontController extends ModuleFrontController
|
|||||||
{
|
{
|
||||||
parent::initContent(); // This initializes the Standard PS Cookie
|
parent::initContent(); // This initializes the Standard PS Cookie
|
||||||
|
|
||||||
$ip = $_SERVER['REMOTE_ADDR'];
|
$ip = BotLogger::getRealIp();
|
||||||
$return_url = urldecode(Tools::getValue('return_url'));
|
$return_url = urldecode(Tools::getValue('return_url'));
|
||||||
|
|
||||||
// Sanity check on return URL to prevent open redirect vulnerabilities
|
// Sanity check on return URL to prevent open redirect vulnerabilities
|
||||||
|
|||||||
Reference in New Issue
Block a user