Implement CSRF protection for user invitation and management actions; add QR code generation script; secure internal includes directory with .htaccess

This commit is contained in:
2026-09-11 14:46:08 +02:00
parent 55618ffaed
commit cf0ceeca82
18 changed files with 413 additions and 163 deletions
+9
View File
@@ -0,0 +1,9 @@
# Beveiliging van de interne includes map (Pen Test Beveiliging)
<IfModule authz_core_module>
Require all denied
</IfModule>
<IfModule !authz_core_module>
Order deny,allow
Deny from all
</IfModule>
+49
View File
@@ -568,6 +568,55 @@ function verify_csrf_token(?string $token): bool {
return !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
/**
* Rate limiting helpers via app_cache
*/
function fociz_check_rate_limit(string $actionKey, int $maxAttempts = 5, int $decaySeconds = 900): array {
require_once __DIR__ . '/cache.php';
$cacheKey = 'rl_' . hash('sha256', $actionKey);
$data = fociz_cache_get($cacheKey);
$attempts = 0;
$expiresAt = time() + $decaySeconds;
if (is_array($data)) {
$attempts = (int)($data['attempts'] ?? 0);
$expiresAt = (int)($data['expires_at'] ?? $expiresAt);
}
$allowed = ($attempts < $maxAttempts);
$remaining = max(0, $maxAttempts - $attempts);
$retryAfter = max(0, $expiresAt - time());
return [
'allowed' => $allowed,
'remaining' => $remaining,
'retry_after' => $retryAfter,
'attempts' => $attempts
];
}
function fociz_hit_rate_limit(string $actionKey, int $decaySeconds = 900): void {
require_once __DIR__ . '/cache.php';
$cacheKey = 'rl_' . hash('sha256', $actionKey);
$data = fociz_cache_get($cacheKey);
$attempts = 1;
$expiresAt = time() + $decaySeconds;
if (is_array($data)) {
$attempts = (int)($data['attempts'] ?? 0) + 1;
$expiresAt = (int)($data['expires_at'] ?? $expiresAt);
}
$ttl = max(1, $expiresAt - time());
fociz_cache_set($cacheKey, ['attempts' => $attempts, 'expires_at' => $expiresAt], $ttl);
}
function fociz_clear_rate_limit(string $actionKey): void {
require_once __DIR__ . '/cache.php';
$cacheKey = 'rl_' . hash('sha256', $actionKey);
fociz_cache_delete($cacheKey);
}
/**
* Genereert een cryptografisch ondertekende OAuth state parameter waarin de user_id en timestamp veilig zijn opgeslagen.
* Dit voorkomt CSRF en maakt sessieherstel mogelijk bij cross-site OAuth redirects.
+29 -2
View File
@@ -9,14 +9,41 @@ require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../cache.php';
/**
* Normaliseert het Jira domein naar een schone URL zonder slash aan het einde
* Normaliseert het Jira domein naar een schone URL zonder slash aan het einde.
* Bevat SSRF-bescherming: weigert loopbacks, localhost en private netwerkadressen.
*/
function jira_normalize_domain(string $domain): string {
$domain = trim($domain);
if (empty($domain)) {
return '';
}
if (!preg_match('#^https?://#i', $domain)) {
$domain = 'https://' . $domain;
}
return rtrim($domain, '/');
$domain = rtrim($domain, '/');
// Parseer en valideer de host ter voorkoming van SSRF (Server-Side Request Forgery)
$parsed = parse_url($domain);
$host = $parsed['host'] ?? '';
if (empty($host)) {
return '';
}
$lowerHost = strtolower($host);
// Weiger localhost en local domains
if ($lowerHost === 'localhost' || str_ends_with($lowerHost, '.localhost') || str_ends_with($lowerHost, '.local') || str_ends_with($lowerHost, '.internal')) {
return '';
}
// Weiger private IP-ranges en loopback adressen (bijv. 127.0.0.1, 192.168.x, 10.x, 172.16.x, 169.254.x)
if (filter_var($host, FILTER_VALIDATE_IP)) {
if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return '';
}
}
return $domain;
}
/**
+14 -3
View File
@@ -35,15 +35,26 @@ class FocizMailer {
$logs[] = date('H:i:s') . ' ' . $msg;
};
// Saniteer e-mailadressen en headers tegen CRLF en SMTP command injection
$toEmail = str_replace(["\r", "\n", "\0"], '', trim($toEmail));
$toName = str_replace(["\r", "\n", "\0"], '', trim($toName));
$subject = str_replace(["\r", "\n", "\0"], '', trim($subject));
if (!filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
$err = "Ongeldig e-mailadres voor verzending: {$toEmail}";
$log("FOUT: {$err}");
return ['success' => false, 'error' => $err, 'logs' => $logs];
}
// 1. Configuratieparameters ophalen
$host = $options['host'] ?? (defined('SMTP_HOST') ? SMTP_HOST : 'mail.fociz.nl');
$port = (int)($options['port'] ?? (defined('SMTP_PORT') ? SMTP_PORT : 587));
$username = $options['username'] ?? (defined('SMTP_USER') ? SMTP_USER : 'info@fociz.nl');
$password = $options['password'] ?? (defined('SMTP_PASS') ? SMTP_PASS : '');
$secure = strtolower($options['secure'] ?? (defined('SMTP_SECURE') ? SMTP_SECURE : 'tls'));
$fromEmail = $options['from_email'] ?? (defined('SMTP_FROM_EMAIL') ? SMTP_FROM_EMAIL : 'no-reply@fociz.nl');
$fromName = $options['from_name'] ?? (defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Fociz');
$replyTo = $options['reply_to'] ?? (defined('SMTP_REPLY_TO') ? SMTP_REPLY_TO : 'info@fociz.nl');
$fromEmail = str_replace(["\r", "\n", "\0"], '', trim($options['from_email'] ?? (defined('SMTP_FROM_EMAIL') ? SMTP_FROM_EMAIL : 'no-reply@fociz.nl')));
$fromName = str_replace(["\r", "\n", "\0"], '', trim($options['from_name'] ?? (defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Fociz')));
$replyTo = str_replace(["\r", "\n", "\0"], '', trim($options['reply_to'] ?? (defined('SMTP_REPLY_TO') ? SMTP_REPLY_TO : 'info@fociz.nl')));
$timeout = (int)($options['timeout'] ?? 15);
// Technische afzender voor de SMTP envelope (moet doorgaans overeenkomen met het geauthenticeerde account)
+8 -1
View File
@@ -110,7 +110,14 @@ function get_workday_settings(): array {
*/
function get_ai_settings(): array {
$envKey = fociz_env('OPENAI_API_KEY', '');
$savedKey = get_system_setting('ai_api_key', '');
$savedKeyRaw = get_system_setting('ai_api_key', '');
$savedKey = '';
if (!empty($savedKeyRaw)) {
if (!function_exists('fociz_decrypt')) {
require_once __DIR__ . '/auth.php';
}
$savedKey = fociz_decrypt($savedKeyRaw);
}
$apiKey = !empty($savedKey) ? $savedKey : $envKey;
return [
+4 -6
View File
@@ -107,14 +107,12 @@ class TOTP {
}
/**
* Genereert een QR code afbeelding URL (via QuickChart HTTPS)
* Genereert een veilige client-side QR code (geen externe API calls meer ter bescherming van TOTP secrets)
*/
public static function getQrCodeUrl(string $otpAuthUrl, int $size = 220): string {
return sprintf(
'https://quickchart.io/qr?text=%s&size=%d&margin=1&ecLevel=M',
urlencode($otpAuthUrl),
$size
);
// Externe API (quickchart.io) verwijderd om te voorkomen dat 2FA secrets uitlekken naar derden.
// QR rendering gebeurt nu 100% lokaal via /assets/js/qrcode.min.js in de browser.
return '';
}
/**