0, 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); } function send_private_no_store_headers(): void { if (headers_sent()) { return; } header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: same-origin'); } function csrf_token(): string { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } function csrf_field(): string { return ''; } function verify_csrf_token(?string $token): bool { return is_string($token) && isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); } function require_valid_csrf_for_post(): void { if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { return; } if (verify_csrf_token($_POST['csrf_token'] ?? null)) { return; } http_response_code(419); exit('Invalid CSRF token.'); } function enable_csrf_form_injection(): void { if (PHP_SAPI === 'cli' || defined('FINANCIAL_CSRF_INJECTION_ENABLED')) { return; } define('FINANCIAL_CSRF_INJECTION_ENABLED', true); ob_start(static function (string $buffer): string { if ( stripos($buffer, ']*)>/i', static function (array $matches) use ($field): string { $tag = $matches[0]; if ( stripos($tag, 'method="post"') === false && stripos($tag, "method='post'") === false && !preg_match('/method\s*=\s*post/i', $tag) ) { return $tag; } if (stripos($tag, 'csrf_token') !== false) { return $tag; } return $tag . $field; }, $buffer ) ?? $buffer; }); } function throttle_login_attempts(string $username): void { $key = 'login_attempts_' . hash('sha256', strtolower($username) . '|' . ($_SERVER['REMOTE_ADDR'] ?? '')); $now = time(); $attempt = $_SESSION[$key] ?? ['count' => 0, 'first_at' => $now]; if (($now - (int)$attempt['first_at']) > 900) { $attempt = ['count' => 0, 'first_at' => $now]; } if ((int)$attempt['count'] >= 8) { throw new RuntimeException('로그인 시도가 많습니다. 잠시 후 다시 시도하세요.'); } $attempt['count']++; $_SESSION[$key] = $attempt; } function clear_login_attempts(string $username): void { $key = 'login_attempts_' . hash('sha256', strtolower($username) . '|' . ($_SERVER['REMOTE_ADDR'] ?? '')); unset($_SESSION[$key]); } function login_user(array $user, bool $remember = false): void { session_regenerate_id(true); $_SESSION['user_id'] = (int)$user['id']; $_SESSION['username'] = $user['username']; if ($remember) { $token = bin2hex(random_bytes(32)); $tokenHash = hash('sha256', $token); $expiresAt = date('Y-m-d H:i:s', strtotime('+30 days')); $pdo = db(); $stmt = $pdo->prepare(" UPDATE users SET remember_token = ?, remember_expires_at = ? WHERE id = ? "); $stmt->execute([$tokenHash, $expiresAt, $user['id']]); setcookie( 'remember_token', $token, [ 'expires' => strtotime('+30 days'), 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ] ); } } function logout_user(): void { if (!empty($_SESSION['user_id'])) { $pdo = db(); $stmt = $pdo->prepare(" UPDATE users SET remember_token = NULL, remember_expires_at = NULL WHERE id = ? "); $stmt->execute([$_SESSION['user_id']]); } setcookie( 'remember_token', '', [ 'expires' => time() - 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ] ); $_SESSION = []; if (ini_get('session.use_cookies')) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'] ?? '', $params['secure'] ?? false, $params['httponly'] ?? false); } session_destroy(); } function try_auto_login(): void { if (!empty($_SESSION['user_id'])) { return; } if (empty($_COOKIE['remember_token'])) { return; } $token = $_COOKIE['remember_token']; $tokenHash = hash('sha256', $token); $pdo = db(); $stmt = $pdo->prepare(" SELECT * FROM users WHERE remember_token = ? AND remember_expires_at IS NOT NULL AND remember_expires_at > NOW() LIMIT 1 "); $stmt->execute([$tokenHash]); $user = $stmt->fetch(); if (!$user) { setcookie( 'remember_token', '', [ 'expires' => time() - 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ] ); return; } $_SESSION['user_id'] = (int)$user['id']; $_SESSION['username'] = $user['username']; $newToken = bin2hex(random_bytes(32)); $newHash = hash('sha256', $newToken); $expiresAt = date('Y-m-d H:i:s', strtotime('+30 days')); $stmt = $pdo->prepare(" UPDATE users SET remember_token = ?, remember_expires_at = ? WHERE id = ? "); $stmt->execute([$newHash, $expiresAt, $user['id']]); setcookie( 'remember_token', $newToken, [ 'expires' => strtotime('+30 days'), 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ] ); } function check_auth(): void { try_auto_login(); if (empty($_SESSION['user_id'])) { header('Location: /login.php'); exit; } send_private_no_store_headers(); } function user_id(): int { return (int)($_SESSION['user_id'] ?? 0); } send_private_no_store_headers(); require_valid_csrf_for_post(); enable_csrf_form_injection();