285 lines
7.0 KiB
PHP
285 lines
7.0 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/db.php';
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_set_cookie_params([
|
|
'lifetime' => 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 '<input type="hidden" name="csrf_token" value="' .
|
|
htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8') .
|
|
'">';
|
|
}
|
|
|
|
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, '<form') === false ||
|
|
!preg_match('/method\s*=\s*["\']?post/i', $buffer)
|
|
) {
|
|
return $buffer;
|
|
}
|
|
|
|
$field = csrf_field();
|
|
|
|
return preg_replace_callback(
|
|
'/<form\b([^>]*)>/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();
|