Fix security vulnerabilities, remove dead code, and improve code quality

- Fix path traversal with realpath() validation in getPage() and executePhpFile()
- Remove insecure JWT secret fallback, require JWT_SECRET env var
- Fix IP spoofing by only trusting proxy headers from configured proxies
- Add Secure/HttpOnly/SameSite flags to all cookies
- Use env var for debug mode instead of hardcoded true
- Fix operator precedence bug in MQTTTracker track_user_flows check
- Remove dead code: duplicate is_dir() block, unused scanForPageNames()
- Remove htmlspecialchars() from filesystem path operations
- Remove duplicate require_once calls and redundant autoloader includes
- Fix unclosed </div> in getDirectoryListing()
- Escape breadcrumb titles and add lang param to search result URLs
- Make language prefixes dynamic from config instead of hardcoded nl|en
- Make HTML lang attribute dynamic, add go_to translation key
- Add aria-label/aria-expanded to sidebar toggle for accessibility
- Fix event listener leak in app.js using event delegation
- Remove console.log from production code
- Update guides (NL/EN) with sidebar toggle documentation
- Add TODO.md documenting all identified improvements
This commit is contained in:
2026-02-16 15:05:27 +01:00
parent e3a3cc5b6d
commit 60276cdccd
11 changed files with 190 additions and 152 deletions

View File

@@ -67,7 +67,13 @@ class MQTTTracker
}
$sessionId = uniqid('cms_', true);
setcookie('cms_session_id', $sessionId, time() + $this->config['session_timeout'], '/');
setcookie('cms_session_id', $sessionId, [
'expires' => time() + $this->config['session_timeout'],
'path' => '/',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax'
]);
return $sessionId;
}
@@ -112,8 +118,20 @@ class MQTTTracker
];
// Update tracking cookies
setcookie('cms_previous_page', $pageUrl, time() + $this->config['session_timeout'], '/');
setcookie('cms_page_timestamp', time(), time() + $this->config['session_timeout'], '/');
setcookie('cms_previous_page', $pageUrl, [
'expires' => time() + $this->config['session_timeout'],
'path' => '/',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax'
]);
setcookie('cms_page_timestamp', (string) time(), [
'expires' => time() + $this->config['session_timeout'],
'path' => '/',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax'
]);
$this->publishMessage('page_visit', $pageData);
}
@@ -128,7 +146,7 @@ class MQTTTracker
private function trackUserFlow(): void
{
if (!$this->config['track_user_flows'] ?? true) {
if (!($this->config['track_user_flows'] ?? true)) {
return;
}
@@ -210,22 +228,30 @@ class MQTTTracker
private function getClientIp(): string
{
// Check Cloudflare header first if present
if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
$ipKeys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
// Only trust REMOTE_ADDR by default - proxy headers can be spoofed
// Configure trusted_proxies in config to enable proxy header support
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$trustedProxies = $this->config['trusted_proxies'] ?? [];
foreach ($ipKeys as $key) {
if (!empty($_SERVER[$key])) {
$ips = explode(',', $_SERVER[$key]);
// Return the first IP in the list (client IP)
return trim($ips[0]);
if (!empty($trustedProxies) && in_array($remoteAddr, $trustedProxies)) {
// Only trust proxy headers when request comes from a known proxy
if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
$ipKeys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];
foreach ($ipKeys as $key) {
if (!empty($_SERVER[$key])) {
$ips = explode(',', $_SERVER[$key]);
$ip = trim($ips[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
return $remoteAddr;
}
private function publishMessage(string $topic, array $data): void