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:
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user