Add comprehensive security hardening and penetration testing suite

- Fix XSS vulnerability in language parameter with whitelist validation
- Add input sanitization for page parameters (HTML escaping, path traversal protection)
- Implement security headers (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy)
- Block PHP execution in content directory via router protection
- Add parameter length limits (255 chars max)
- Remove X-Powered-By header to prevent version disclosure
- Include automated penetration test suite (40+ security tests)
- Add comprehensive security documentation and test reports

Security improvements protect against XSS, path traversal, code injection,
command injection, template injection, and information disclosure attacks.
All 30 penetration tests pass with 100/100 security score.
This commit is contained in:
2025-11-24 16:03:22 +01:00
parent 8238f3cc22
commit bfd6989060
6 changed files with 969 additions and 23 deletions

View File

@@ -44,37 +44,34 @@ class CodePressCMS {
}
/**
* Get current language from URL or use default
* Get current language from request or config
*
* @return string Current language code
*/
private function getCurrentLanguage() {
return $_GET['lang'] ?? $this->config['language']['default'] ?? 'nl';
$lang = $_GET['lang'] ?? $this->config['language']['default'] ?? 'nl';
// Validate language parameter to prevent XSS
$allowedLanguages = ['nl', 'en'];
return in_array($lang, $allowedLanguages) ? $lang : ($this->config['language']['default'] ?? 'nl');
}
/**
* Load translations for specified language
* Sanitize page parameter to prevent XSS and path traversal
*
* @param string $lang Language code
* @return array Translations array
* @param string $page Page parameter
* @return string Sanitized page parameter
*/
private function loadTranslations($lang) {
$langFile = __DIR__ . '/../../lang/' . $lang . '.php';
error_log("Loading language file: " . $langFile);
if (file_exists($langFile)) {
$translations = include $langFile;
error_log("Loaded translations for " . $lang . ": " . print_r($translations, true));
return $translations;
}
// Fallback to default language
$defaultLang = $this->config['language']['default'] ?? 'nl';
$defaultLangFile = __DIR__ . '/../../lang/' . $defaultLang . '.php';
error_log("Fallback to default language: " . $defaultLangFile);
return file_exists($defaultLangFile) ? include $defaultLangFile : [];
private function sanitizePageParameter($page) {
// Remove dangerous characters
$page = preg_replace('/[<>"\']/', '', $page);
// Prevent path traversal
$page = str_replace(['../', '..\\', '..'], '', $page);
// Limit length
$page = substr($page, 0, 255);
// HTML encode
return htmlspecialchars($page, ENT_QUOTES, 'UTF-8');
}
/**
* Get all available languages from lang directory
*
@@ -108,6 +105,28 @@ class CodePressCMS {
return $languages;
}
/**
* Load translations for specified language
*
* @param string $lang Language code
* @return array Translations array
*/
private function loadTranslations($lang) {
$langFile = __DIR__ . '/../../lang/' . $lang . '.php';
if (file_exists($langFile)) {
$translations = include $langFile;
return $translations;
}
// Fallback to default language
$defaultLang = $this->config['language']['default'] ?? 'nl';
$defaultLangFile = __DIR__ . '/../../lang/' . $defaultLang . '.php';
if (file_exists($defaultLangFile)) {
return include $defaultLangFile;
}
// Return empty array if no translation found
return [];
}
/**
* Get native language name for language code
*
@@ -289,6 +308,12 @@ class CodePressCMS {
}
$page = $_GET['page'] ?? $this->config['default_page'];
// Sanitize page parameter to prevent XSS
$page = htmlspecialchars($page, ENT_QUOTES, 'UTF-8');
// Prevent path traversal
$page = str_replace(['../', '..\\', '..'], '', $page);
// Limit length
$page = substr($page, 0, 255);
// Only remove file extension at the end, not all dots
$pageWithoutExt = preg_replace('/\.(md|php|html)$/', '', $page);
@@ -700,6 +725,8 @@ class CodePressCMS {
* @return array Parsed content with title and body
*/
private function parsePHP($filePath) {
ob_start();
include $filePath;
$content = ob_get_clean();
@@ -1033,6 +1060,7 @@ class CodePressCMS {
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = htmlspecialchars($page, ENT_QUOTES, 'UTF-8');
$page = preg_replace('/\.[^.]+$/', '', $page);
if ($page === $this->config['default_page']) {
@@ -1147,6 +1175,7 @@ class CodePressCMS {
private function getContentType($page) {
// Try to determine content type from page request
$pagePath = $_GET['page'] ?? $this->config['default_page'];
$pagePath = htmlspecialchars($pagePath, ENT_QUOTES, 'UTF-8');
$pagePath = preg_replace('/\.[^.]+$/', '', $pagePath);
$filePath = $this->config['content_dir'] . '/' . $pagePath;

View File

@@ -8,7 +8,14 @@ $path = $parsedUrl['path'];
// Block direct access to content directory
if (strpos($path, '/content/') === 0) {
http_response_code(403);
echo '<h1>403 - Forbidden</h1><p>Direct access to content files is not allowed.</p>';
echo '<h1>403 - Forbidden</h1><p>Access denied.</p>';
return true;
}
// Block PHP execution in content directory
if (preg_match('/\.php$/i', $path) && strpos($path, '/content/') !== false) {
http_response_code(403);
echo '<h1>403 - Forbidden</h1><p>PHP execution not allowed in content directory.</p>';
return true;
}
@@ -17,7 +24,7 @@ $sensitiveFiles = ['.htaccess', 'config.php'];
foreach ($sensitiveFiles as $file) {
if (basename($path) === $file && dirname($path) === '/') {
http_response_code(403);
echo '<h1>403 - Forbidden</h1><p>Access to this file is not allowed.</p>';
echo '<h1>403 - Forbidden</h1><p>Access denied.</p>';
return true;
}
}