false, 'trusted_proxies' => [], 'setup_key' => '', ]; } /** True when the client reached us over HTTPS (directly or via a trusted reverse proxy). */ function is_https(): bool { if (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') { return true; } if ((int)($_SERVER['SERVER_PORT'] ?? 0) === 443) { return true; } $proxies = security_config()['trusted_proxies']; if ($proxies && in_array($_SERVER['REMOTE_ADDR'] ?? '', $proxies, true)) { return strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https'; } return false; } /** Client IP; X-Forwarded-For is honoured only when the request came from a trusted proxy. */ function client_ip(): string { $remote = (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); $proxies = security_config()['trusted_proxies']; if ($proxies && in_array($remote, $proxies, true) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $chain = array_map('trim', explode(',', (string)$_SERVER['HTTP_X_FORWARDED_FOR'])); // right-most address that is not one of our proxies foreach (array_reverse($chain) as $ip) { if (filter_var($ip, FILTER_VALIDATE_IP) && !in_array($ip, $proxies, true)) { return substr($ip, 0, 45); } } } return substr($remote, 0, 45); } /** Per-request nonce for the few inline scripts (Content-Security-Policy). */ function csp_nonce(): string { static $nonce = null; if ($nonce === null) { $nonce = rtrim(strtr(base64_encode(random_bytes(18)), '+/', '-_'), '='); } return $nonce; } function send_security_headers(bool $sensitive = false): void { if (headers_sent()) { return; } header_remove('X-Powered-By'); $csp = [ "default-src 'none'", "script-src 'self' 'nonce-" . csp_nonce() . "'", "style-src 'self'", "img-src 'self'", "font-src 'self'", "connect-src 'self'", "manifest-src 'self'", "form-action 'self'", "frame-ancestors 'none'", "base-uri 'none'", "object-src 'none'", ]; if (is_https()) { $csp[] = 'upgrade-insecure-requests'; header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); } header('Content-Security-Policy: ' . implode('; ', $csp)); header('X-Frame-Options: DENY'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: same-origin'); header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()'); header('Cross-Origin-Opener-Policy: same-origin'); header('Cross-Origin-Resource-Policy: same-origin'); header('X-Permitted-Cross-Domain-Policies: none'); // Pages always revalidate so they reference the newest versioned assets; // admin pages are never stored at all. header($sensitive ? 'Cache-Control: no-store, max-age=0' : 'Cache-Control: no-cache, must-revalidate'); if ($sensitive) { header('Pragma: no-cache'); } } function security_bootstrap(): void { static $done = false; if ($done || PHP_SAPI === 'cli') { return; } $done = true; // Never leak errors or stack traces to the client. ini_set('display_errors', '0'); ini_set('display_startup_errors', '0'); ini_set('log_errors', '1'); ini_set('expose_php', '0'); error_reporting(E_ALL); set_exception_handler(function (Throwable $e): void { error_log('[serubi] ' . get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); if (!headers_sent()) { http_response_code(500); header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: no-store'); } echo 'Terjadi kesalahan

Terjadi kesalahan pada server. Silakan coba lagi beberapa saat lagi.

'; }); // Hardened session settings (before any session_start()). ini_set('session.use_strict_mode', '1'); ini_set('session.use_only_cookies', '1'); ini_set('session.use_trans_sid', '0'); ini_set('session.cookie_httponly', '1'); ini_set('session.sid_length', '48'); ini_set('session.sid_bits_per_character', '5'); ini_set('session.gc_maxlifetime', '28800'); // Only GET, HEAD and POST are meaningful here (blocks TRACE/PUT/DELETE/OPTIONS...). $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); if (!in_array($method, ['GET', 'HEAD', 'POST'], true)) { header('Allow: GET, HEAD, POST'); http_response_code(405); exit; } // Optional HTTP -> HTTPS redirect for production. if (security_config()['force_https'] && !is_https()) { $host = preg_replace('/[^A-Za-z0-9.\-:\[\]]/', '', (string)($_SERVER['HTTP_HOST'] ?? '')); $uri = (string)($_SERVER['REQUEST_URI'] ?? '/'); if ($host !== '' && $uri !== '' && $uri[0] === '/') { header('Location: https://' . $host . $uri, true, 301); exit; } } }