CodePress CMS v1.8.0: BotGuard security engine, HAProxy docs & ARIA fix
- Implement BotGuard security engine (Bot, AI, Scraper & Empty UA blocking) - Add Admin Security page (/admin/security) with toggles, rate limiter & block/allowlists - Add per-IP RateLimiter handoff in index.php with HTTP 429 response - Add dynamic /robots.txt generation and noai/noimageai meta tags - Add RequestLogger status column and blocked badges in admin request logs - Fix ARIAComponents.php syntax errors on lines 67, 137, 262 - Add HAProxy / PFSense bot blocking & IP forwarding guide (docs/haproxy-bot-blocking.md) - Update version to 1.8.0 with release notes in version.php and guides
This commit is contained in:
@@ -88,6 +88,10 @@ switch ($route) {
|
||||
handleConfig($auth, $appConfig);
|
||||
break;
|
||||
|
||||
case 'security':
|
||||
handleSecurity($auth, $appConfig);
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
handleUpdate($auth, $appConfig);
|
||||
break;
|
||||
@@ -747,6 +751,64 @@ function handleConfig(AdminAuth $auth, array $config): void
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
|
||||
function handleSecurity(AdminAuth $auth, array $config): void
|
||||
{
|
||||
$user = $auth->getCurrentUser();
|
||||
$csrf = $auth->getCsrfToken();
|
||||
$configJson = $config['config_json'];
|
||||
$message = '';
|
||||
$messageType = '';
|
||||
|
||||
// Load current config
|
||||
$configData = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
||||
if (!is_array($configData)) $configData = [];
|
||||
|
||||
$sec = $configData['security'] ?? [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
||||
$message = 'Ongeldige CSRF token.';
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
$configData['security']['block_ai_bots'] = !empty($_POST['block_ai_bots']);
|
||||
$configData['security']['block_scrapers'] = !empty($_POST['block_scrapers']);
|
||||
$configData['security']['block_search_engines'] = !empty($_POST['block_search_engines']);
|
||||
$configData['security']['block_empty_user_agent'] = !empty($_POST['block_empty_user_agent']);
|
||||
$configData['security']['rate_limit_enabled'] = !empty($_POST['rate_limit_enabled']);
|
||||
$configData['security']['rate_limit_max'] = max(10, min(1000, (int)($_POST['rate_limit_max'] ?? 60)));
|
||||
$configData['security']['rate_limit_window'] = max(10, min(3600, (int)($_POST['rate_limit_window'] ?? 60)));
|
||||
|
||||
$parseLines = function($text) {
|
||||
$lines = explode("\n", str_replace("\r", "", $text));
|
||||
$clean = [];
|
||||
foreach ($lines as $line) {
|
||||
$item = trim($line);
|
||||
if ($item !== '') {
|
||||
$clean[] = $item;
|
||||
}
|
||||
}
|
||||
return array_values(array_unique($clean));
|
||||
};
|
||||
|
||||
$configData['security']['custom_blocked_agents'] = $parseLines($_POST['custom_blocked_agents'] ?? '');
|
||||
$configData['security']['blocked_ips'] = $parseLines($_POST['blocked_ips'] ?? '');
|
||||
$configData['security']['allowed_ips'] = $parseLines($_POST['allowed_ips'] ?? '');
|
||||
|
||||
file_put_contents($configJson, json_encode($configData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
adminLog($config, 'info', $user['username'] . ' wijzigde beveiligingsinstellingen');
|
||||
$message = 'Beveiligingsinstellingen opgeslagen.';
|
||||
$messageType = 'success';
|
||||
$sec = $configData['security'];
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../cms/core/class/BotGuard.php';
|
||||
$robotsPreview = BotGuard::generateRobotsTxt($sec);
|
||||
|
||||
$route = 'security';
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
|
||||
function handleTheme(AdminAuth $auth, array $config): void
|
||||
{
|
||||
$user = $auth->getCurrentUser();
|
||||
|
||||
+68
-9
@@ -82,6 +82,13 @@ if (strpos($path, '/-assets/') === 0) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Serve dynamic robots.txt
|
||||
if ($path === '/robots.txt') {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
echo BotGuard::generateRobotsTxt($config['security'] ?? []);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Block direct access to content files
|
||||
if (strpos($path, '/content/') === 0) {
|
||||
http_response_code(403);
|
||||
@@ -89,16 +96,54 @@ if (strpos($path, '/content/') === 0) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load RequestLogger for bot detection and request logging
|
||||
// Load RequestLogger for IP & request logging
|
||||
require_once __DIR__ . '/../cms/core/class/RequestLogger.php';
|
||||
|
||||
// Bot detection — block known bots/AI scrapers early
|
||||
if (RequestLogger::detectBot() !== null) {
|
||||
http_response_code(403);
|
||||
echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>403 Forbidden</title><meta name="robots" content="noindex,nofollow"></head><body><h1>403 Forbidden</h1><p>Access denied.</p></body></html>';
|
||||
exit;
|
||||
// Execute security checks & rate limiting
|
||||
$clientIp = RequestLogger::getClientIp();
|
||||
$secSettings = $config['security'] ?? [
|
||||
'block_ai_bots' => true,
|
||||
'block_scrapers' => true,
|
||||
'block_search_engines' => false,
|
||||
'block_empty_user_agent' => true,
|
||||
'rate_limit_enabled' => true,
|
||||
'rate_limit_max' => 60,
|
||||
'rate_limit_window' => 60,
|
||||
];
|
||||
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
$isAllowedIp = in_array($clientIp, $secSettings['allowed_ips'] ?? [], true);
|
||||
$requestStatus = 'ok';
|
||||
|
||||
if (!$isAllowedIp) {
|
||||
// 1. IP Blocklist
|
||||
if (in_array($clientIp, $secSettings['blocked_ips'] ?? [], true)) {
|
||||
$requestStatus = 'blocked:ip';
|
||||
}
|
||||
|
||||
// 2. BotGuard User-Agent check
|
||||
if ($requestStatus === 'ok') {
|
||||
$botBlockReason = BotGuard::shouldBlock($userAgent, $secSettings);
|
||||
if ($botBlockReason !== null) {
|
||||
$requestStatus = $botBlockReason;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Rate Limiting per IP
|
||||
if ($requestStatus === 'ok' && !empty($secSettings['rate_limit_enabled'])) {
|
||||
$cacheDir = dirname(__DIR__) . '/admin/storage/cache';
|
||||
$rateLimiter = new RateLimiter(
|
||||
(int)($secSettings['rate_limit_max'] ?? 60),
|
||||
(int)($secSettings['rate_limit_window'] ?? 60),
|
||||
new FileCache($cacheDir)
|
||||
);
|
||||
if (!$rateLimiter->isAllowed($clientIp)) {
|
||||
$requestStatus = 'blocked:ratelimit';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate CMS instance
|
||||
$cms = new CodePressCMS($config);
|
||||
|
||||
// Log page view (not for media/assets)
|
||||
@@ -112,12 +157,26 @@ if (!str_starts_with($path, '/-media/') && !str_starts_with($path, '/-assets/'))
|
||||
$logger = new RequestLogger($requestLogFile);
|
||||
$logger->log(
|
||||
$_GET['page'] ?? $cms->getEffectiveDefaultPage(),
|
||||
RequestLogger::getClientIp(),
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
$clientIp,
|
||||
$userAgent,
|
||||
$_SERVER['HTTP_REFERER'] ?? '',
|
||||
$loggedInUser,
|
||||
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
|
||||
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
|
||||
$requestStatus
|
||||
);
|
||||
}
|
||||
|
||||
// Block request if status is not ok
|
||||
if ($requestStatus !== 'ok') {
|
||||
if ($requestStatus === 'blocked:ratelimit') {
|
||||
http_response_code(429);
|
||||
header('Retry-After: ' . (int)($secSettings['rate_limit_window'] ?? 60));
|
||||
echo '<!DOCTYPE html><html lang="nl"><head><meta charset="UTF-8"><title>429 Te Veel Verzoeken</title><meta name="robots" content="noindex,nofollow"></head><body><h1>429 Te Veel Verzoeken</h1><p>Probeer het over een minuut opnieuw.</p></body></html>';
|
||||
} else {
|
||||
http_response_code(403);
|
||||
echo '<!DOCTYPE html><html lang="nl"><head><meta charset="UTF-8"><title>403 Toegang Geweigerd</title><meta name="robots" content="noindex,nofollow"></head><body><h1>403 Toegang Geweigerd</h1><p>Toegang geweigerd.</p></body></html>';
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
$cms->render();
|
||||
Reference in New Issue
Block a user