diff --git a/adm/dbupgrade.php b/adm/dbupgrade.php index 467bd414a..ae11ece8f 100644 --- a/adm/dbupgrade.php +++ b/adm/dbupgrade.php @@ -351,6 +351,29 @@ if (defined('G5_USE_SHOP') && G5_USE_SHOP) { } } +// 자동 로그인 토큰 테이블 생성 (KVE-2026-0610: 추측 가능한 자동 로그인 쿠키 위조 방지) +// 다중 디바이스 지원을 위해 회원당 여러 토큰을 별도 테이블로 관리 +if (!isset($g5['member_auto_login_table'])) { + $g5['member_auto_login_table'] = G5_TABLE_PREFIX.'member_auto_login'; +} +if (!sql_query(" DESC `{$g5['member_auto_login_table']}` ", false)) { + sql_query(" CREATE TABLE IF NOT EXISTS `{$g5['member_auto_login_table']}` ( + `al_id` int(11) NOT NULL auto_increment, + `mb_id` varchar(20) NOT NULL default '', + `al_token` varchar(64) NOT NULL default '', + `al_user_agent` varchar(255) NOT NULL default '', + `al_ip` varchar(45) NOT NULL default '', + `al_created` datetime DEFAULT NULL, + `al_last_used` datetime DEFAULT NULL, + `al_expire` datetime DEFAULT NULL, + PRIMARY KEY (`al_id`), + UNIQUE KEY `al_token` (`al_token`), + KEY `mb_id` (`mb_id`), + KEY `al_expire` (`al_expire`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ", true); + $is_check = true; +} + $is_check = run_replace('admin_dbupgrade', $is_check); $db_upgrade_msg = $is_check ? 'DB 업그레이드가 완료되었습니다.' : '더 이상 업그레이드 할 내용이 없습니다.
현재 DB 업그레이드가 완료된 상태입니다.'; diff --git a/bbs/login_check.php b/bbs/login_check.php index c34c44ce2..6d15b1f5a 100644 --- a/bbs/login_check.php +++ b/bbs/login_check.php @@ -90,10 +90,33 @@ if($config['cf_use_point']) { if (isset($auto_login) && $auto_login) { // 3.27 // 자동로그인 --------------------------- - // 쿠키 한달간 저장 - $key = md5($_SERVER['SERVER_ADDR'] . $_SERVER['SERVER_SOFTWARE'] . $_SERVER['HTTP_USER_AGENT'] . $mb['mb_password']); + // 예측 불가능한 랜덤 토큰을 생성하여 g5_member_auto_login 테이블에 저장. + // 다중 디바이스 지원: 디바이스마다 별도의 토큰 행이 생성되며 서로 독립적으로 동작. + + $auto_token = get_random_token_string(32); // 64자 hex - 쿠키에는 원본 + $auto_token_hash = hash('sha256', $auto_token); // 64자 hex - DB에는 해시 + $auto_ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 255) : ''; + $auto_ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; + $auto_now = G5_TIME_YMDHIS; + $auto_expire = date('Y-m-d H:i:s', G5_SERVER_TIME + 86400 * 31); + + // 만료된 토큰 정리 (해당 회원 한정) + sql_query(" delete from {$g5['member_auto_login_table']} + where mb_id = '{$mb['mb_id']}' + and al_expire < '{$auto_now}' "); + + // 새 토큰 INSERT (해시값만 저장) + sql_query(" insert into {$g5['member_auto_login_table']} + set mb_id = '{$mb['mb_id']}', + al_token = '{$auto_token_hash}', + al_user_agent = '".addslashes($auto_ua)."', + al_ip = '".addslashes($auto_ip)."', + al_created = '{$auto_now}', + al_last_used = '{$auto_now}', + al_expire = '{$auto_expire}' "); + set_cookie('ck_mb_id', $mb['mb_id'], 86400 * 31); - set_cookie('ck_auto', $key, 86400 * 31); + set_cookie('ck_auto', $auto_token, 86400 * 31); // 쿠키에는 원본 토큰 // 자동로그인 end --------------------------- } else { set_cookie('ck_mb_id', '', 0); diff --git a/bbs/logout.php b/bbs/logout.php index 53b87571d..9f4e418d4 100644 --- a/bbs/logout.php +++ b/bbs/logout.php @@ -11,6 +11,18 @@ session_unset(); // 모든 세션변수를 언레지스터 시켜줌 session_destroy(); // 세션해제함 // 자동로그인 해제 -------------------------------- +// DB에 저장된 해당 디바이스의 자동 로그인 토큰만 무효화 +// (다른 디바이스의 자동 로그인은 유지) +// DB에는 토큰의 SHA256 해시가 저장되어 있으므로 비교 시에도 해시값 사용 +$logout_token = get_cookie('ck_auto'); +$logout_mb_id = get_cookie('ck_mb_id'); +if (isset($g5['member_auto_login_table']) && $logout_token && $logout_mb_id && preg_match('/^[a-f0-9]{64}$/', $logout_token)) { + $logout_mb_id = substr(preg_replace("/[^a-zA-Z0-9_]*/", "", $logout_mb_id), 0, 20); + $logout_token_hash = hash('sha256', $logout_token); + sql_query(" delete from {$g5['member_auto_login_table']} + where mb_id = '{$logout_mb_id}' + and al_token = '{$logout_token_hash}' "); +} set_cookie('ck_mb_id', '', 0); set_cookie('ck_auto', '', 0); // 자동로그인 해제 end -------------------------------- diff --git a/common.php b/common.php index cc960894e..466aa9ac8 100644 --- a/common.php +++ b/common.php @@ -165,6 +165,11 @@ if (file_exists($dbconfig_file)) { sql_set_charset(G5_DB_CHARSET, $connect_db); if(defined('G5_MYSQL_SET_MODE') && G5_MYSQL_SET_MODE) sql_query("SET SESSION sql_mode = ''"); if (defined('G5_TIMEZONE')) sql_query(" set time_zone = '".G5_TIMEZONE."'"); + + // 자동 로그인 토큰 테이블 - 기존 dbconfig.php에 정의되지 않은 경우 fallback + if (!isset($g5['member_auto_login_table'])) { + $g5['member_auto_login_table'] = G5_TABLE_PREFIX.'member_auto_login'; + } } else { ?> @@ -556,17 +561,30 @@ if (isset($_SESSION['ss_mb_id']) && $_SESSION['ss_mb_id']) { // 로그인중이 $tmp_mb_id = substr(preg_replace("/[^a-zA-Z0-9_]*/", "", $tmp_mb_id), 0, 20); // 최고관리자는 자동로그인 금지 if (strtolower($tmp_mb_id) !== strtolower($config['cf_admin'])) { - $sql = " select mb_password, mb_intercept_date, mb_leave_date, mb_email_certify, mb_datetime from {$g5['member_table']} where mb_id = '{$tmp_mb_id}' "; - $row = sql_fetch($sql); - if($row['mb_password']){ - $key = md5($_SERVER['SERVER_ADDR'] . $_SERVER['SERVER_SOFTWARE'] . $_SERVER['HTTP_USER_AGENT'] . $row['mb_password']); - // 쿠키에 저장된 키와 같다면 - $tmp_key = get_cookie('ck_auto'); - if ($tmp_key === $key && $tmp_key) { + // 쿠키 값을 g5_member_auto_login 테이블의 토큰과 비교 + // 다중 디바이스 지원을 위해 회원당 여러 토큰을 별도 테이블에 저장 + $tmp_key = get_cookie('ck_auto'); + // 토큰 형식 검증 (64자 hex) - 빈 값/잘못된 형식 차단 + if ($tmp_key && preg_match('/^[a-f0-9]{64}$/', $tmp_key)) { + $tmp_key_hash = hash('sha256', $tmp_key); // 쿠키 값을 해시해서 DB와 비교 + $sql = " select al.al_id, al.al_expire, + m.mb_intercept_date, m.mb_leave_date, m.mb_email_certify, m.mb_datetime + from {$g5['member_auto_login_table']} al + inner join {$g5['member_table']} m on m.mb_id = al.mb_id + where al.mb_id = '{$tmp_mb_id}' + and al.al_token = '{$tmp_key_hash}' + and al.al_expire > '".G5_TIME_YMDHIS."' "; + $row = sql_fetch($sql); + if (isset($row['al_id']) && $row['al_id']) { // 차단, 탈퇴가 아니고 메일인증이 사용이면서 인증을 받았다면 if ($row['mb_intercept_date'] == '' && $row['mb_leave_date'] == '' && (!$config['cf_use_email_certify'] || preg_match('/[1-9]/', $row['mb_email_certify'])) ) { + // 마지막 사용 시간 갱신 + sql_query(" update {$g5['member_auto_login_table']} + set al_last_used = '".G5_TIME_YMDHIS."' + where al_id = '{$row['al_id']}' "); + // 세션에 회원아이디를 저장하여 로그인으로 간주 set_session('ss_mb_id', $tmp_mb_id); if(function_exists('update_auth_session_token')) update_auth_session_token($row['mb_datetime']); @@ -576,9 +594,9 @@ if (isset($_SESSION['ss_mb_id']) && $_SESSION['ss_mb_id']) { // 로그인중이 exit; } } + // $row 배열변수 해제 + unset($row); } - // $row 배열변수 해제 - unset($row); } } // 자동로그인 end --------------------------------------- diff --git a/install/gnuboard5.sql b/install/gnuboard5.sql index fcc6676b6..69a83c219 100644 --- a/install/gnuboard5.sql +++ b/install/gnuboard5.sql @@ -554,6 +554,28 @@ CREATE TABLE IF NOT EXISTS `g5_member` ( ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- -------------------------------------------------------- +-- +-- Table structure for table `g5_member_auto_login` +-- KVE-2026-0610: 자동 로그인 토큰 저장 (다중 디바이스 지원) +-- + +DROP TABLE IF EXISTS `g5_member_auto_login`; +CREATE TABLE IF NOT EXISTS `g5_member_auto_login` ( + `al_id` int(11) NOT NULL auto_increment, + `mb_id` varchar(20) NOT NULL default '', + `al_token` varchar(64) NOT NULL default '', + `al_user_agent` varchar(255) NOT NULL default '', + `al_ip` varchar(45) NOT NULL default '', + `al_created` datetime DEFAULT NULL, + `al_last_used` datetime DEFAULT NULL, + `al_expire` datetime DEFAULT NULL, + PRIMARY KEY (`al_id`), + UNIQUE KEY `al_token` (`al_token`), + KEY `mb_id` (`mb_id`), + KEY `al_expire` (`al_expire`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- -------------------------------------------------------- + -- -- Table structure for table `g5_memo` -- diff --git a/install/install_db.php b/install/install_db.php index d2c2fce98..731d6a3f0 100644 --- a/install/install_db.php +++ b/install/install_db.php @@ -596,6 +596,7 @@ fwrite($f, "\$g5['board_new_table'] = G5_TABLE_PREFIX.'board_new'; // 게시판 fwrite($f, "\$g5['login_table'] = G5_TABLE_PREFIX.'login'; // 로그인 테이블 (접속자수)\n"); fwrite($f, "\$g5['mail_table'] = G5_TABLE_PREFIX.'mail'; // 회원메일 테이블\n"); fwrite($f, "\$g5['member_table'] = G5_TABLE_PREFIX.'member'; // 회원 테이블\n"); +fwrite($f, "\$g5['member_auto_login_table'] = G5_TABLE_PREFIX.'member_auto_login'; // 자동 로그인 토큰 테이블\n"); fwrite($f, "\$g5['memo_table'] = G5_TABLE_PREFIX.'memo'; // 메모 테이블\n"); fwrite($f, "\$g5['poll_table'] = G5_TABLE_PREFIX.'poll'; // 투표 테이블\n"); fwrite($f, "\$g5['poll_etc_table'] = G5_TABLE_PREFIX.'poll_etc'; // 투표 기타의견 테이블\n"); diff --git a/lib/common.lib.php b/lib/common.lib.php index cd2b894e0..000073d7e 100644 --- a/lib/common.lib.php +++ b/lib/common.lib.php @@ -4344,10 +4344,49 @@ function get_token_encryption_key($str=''){ function get_random_token_string($length=6) { - if(function_exists('random_bytes')){ - return bin2hex(random_bytes($length)); + // 사용 가능한 가장 안전한 CSPRNG를 우선순위대로 시도하여 $length 바이트의 무작위 값을 얻는다. + // PHP 5.2.17 ~ 8.x 호환. 모든 단계는 function_exists/defined로 가드되어 구버전에서도 안전. + $bytes = false; + + // 1순위. PHP 7.0+ : random_bytes() — OS의 CSPRNG 직접 사용 (가장 안전) + if (function_exists('random_bytes')) { + $bytes = random_bytes($length); + } + // 2순위. PHP 5.3+ : openssl_random_pseudo_bytes() — OpenSSL 확장이 있으면 사용 + elseif (function_exists('openssl_random_pseudo_bytes')) { + $strong = false; + $tmp = openssl_random_pseudo_bytes($length, $strong); + if ($tmp !== false && $strong === true) { + $bytes = $tmp; + } } + // 3순위. Linux/Unix : /dev/urandom 직접 읽기 (PHP 5.2 호환, OS 차원의 CSPRNG) + if ($bytes === false && DIRECTORY_SEPARATOR === '/' && @is_readable('/dev/urandom')) { + $fp = @fopen('/dev/urandom', 'rb'); + if ($fp !== false) { + $tmp = @fread($fp, $length); + @fclose($fp); + if ($tmp !== false && strlen($tmp) === $length) { + $bytes = $tmp; + } + } + } + + // 4순위. mcrypt 확장 (PHP 5.2 호환, PHP 7.2에서 제거됨) — Windows 등에서 fallback + if ($bytes === false && function_exists('mcrypt_create_iv') && defined('MCRYPT_DEV_URANDOM')) { + $tmp = @mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + if ($tmp !== false && strlen($tmp) === $length) { + $bytes = $tmp; + } + } + + if ($bytes !== false) { + return bin2hex($bytes); + } + + // 최후 수단: 약한 RNG (암호학적 안전성 없음, 기존 동작 호환을 위해 유지) + // 이 경로에 도달하면 시스템에 안전한 난수 소스가 전혀 없는 비정상 환경이므로 점검 필요. $characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; $output = substr(str_shuffle($characters), 0, $length);