From d11110085685d831284a0b506a4a7ba4acae31a5 Mon Sep 17 00:00:00 2001 From: whitedot Date: Thu, 3 Sep 2026 05:47:28 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EB=A7=81=ED=81=AC=20=EC=9C=A0=ED=9A=A8=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adm/config_form.php | 16 ++++++++ adm/config_form_update.php | 4 ++ adm/dbupgrade.php | 9 +++++ bbs/email_certify.php | 25 ++++++++---- bbs/register_email_update.php | 4 +- bbs/register_form_update.php | 4 +- bbs/register_form_update_mail1.php | 3 +- bbs/register_form_update_mail3.php | 1 + install/gnuboard5.sql | 3 +- lib/common.lib.php | 48 ++++++++++++++++++++++++ plugin/social/register_member_update.php | 2 +- 11 files changed, 105 insertions(+), 14 deletions(-) diff --git a/adm/config_form.php b/adm/config_form.php index 7ee6347af..efa6f822a 100644 --- a/adm/config_form.php +++ b/adm/config_form.php @@ -12,6 +12,15 @@ if ($is_admin != 'super') { $sql = " select * from {$g5['config_table']} limit 1"; $config = sql_fetch($sql); +if (!isset($config['cf_email_certify_minutes'])) { + sql_query( + " ALTER TABLE `{$g5['config_table']}` + ADD `cf_email_certify_minutes` INT(11) NOT NULL DEFAULT '60' AFTER `cf_use_email_certify` ", + true + ); + $config['cf_email_certify_minutes'] = 60; +} + if (!isset($config['cf_add_script'])) { sql_query( " ALTER TABLE `{$g5['config_table']}` @@ -1189,6 +1198,13 @@ if ($config['cf_sms_use'] && $config['cf_icode_id'] && $config['cf_icode_pw']) { > 사용 + + + + + 분 + + diff --git a/adm/config_form_update.php b/adm/config_form_update.php index 7ba01db8e..519f38d4a 100644 --- a/adm/config_form_update.php +++ b/adm/config_form_update.php @@ -56,6 +56,7 @@ if (isset($_POST['cf_intercept_ip']) && $_POST['cf_intercept_ip']) { $check_keys = array( 'cf_use_email_certify' => 'int', + 'cf_email_certify_minutes' => 'int', 'cf_use_homepage' => 'int', 'cf_req_homepage' => 'int', 'cf_use_tel' => 'int', @@ -178,6 +179,8 @@ foreach ($check_keys as $k => $v) { } } +$_POST['cf_email_certify_minutes'] = max(0, $_POST['cf_email_certify_minutes']); + // 본인확인을 사용할 경우 아이핀, 휴대폰인증 중 하나는 선택되어야 함 if ($_POST['cf_cert_use'] && !$_POST['cf_cert_ipin'] && !$_POST['cf_cert_hp'] && !$_POST['cf_cert_simple']) { alert('본인확인을 위해 아이핀, 휴대폰 본인확인, KG이니시스 간편인증 서비스 중 하나 이상 선택해 주십시오.'); @@ -226,6 +229,7 @@ $sql = " update {$g5['config_table']} cf_point_term = '{$_POST['cf_point_term']}', cf_use_copy_log = '{$_POST['cf_use_copy_log']}', cf_use_email_certify = '{$_POST['cf_use_email_certify']}', + cf_email_certify_minutes = '{$_POST['cf_email_certify_minutes']}', cf_login_point = '{$_POST['cf_login_point']}', cf_cut_name = '{$_POST['cf_cut_name']}', cf_nick_modify = '{$_POST['cf_nick_modify']}', diff --git a/adm/dbupgrade.php b/adm/dbupgrade.php index 5d6a80289..1dbd0e60a 100644 --- a/adm/dbupgrade.php +++ b/adm/dbupgrade.php @@ -9,6 +9,15 @@ include_once('./admin.head.php'); $is_check = false; +// 메일 인증 링크 유효시간 설정 추가 +if (!isset($config['cf_email_certify_minutes'])) { + sql_query("ALTER TABLE `{$g5['config_table']}` + ADD `cf_email_certify_minutes` int(11) NOT NULL DEFAULT '60' AFTER `cf_use_email_certify` + ", true); + + $is_check = true; +} + //소셜 로그인 관련 필드 및 구글 리챕챠 필드 추가 if(!isset($config['cf_social_login_use'])) { sql_query("ALTER TABLE `{$g5['config_table']}` diff --git a/bbs/email_certify.php b/bbs/email_certify.php index e314ff24e..b315ec7d4 100644 --- a/bbs/email_certify.php +++ b/bbs/email_certify.php @@ -6,8 +6,9 @@ if(function_exists('check_mail_bot')){ check_mail_bot($_SERVER['REMOTE_ADDR']); $mb_id = isset($_GET['mb_id']) ? trim($_GET['mb_id']) : ''; $mb_md5 = isset($_GET['mb_md5']) ? trim($_GET['mb_md5']) : ''; +$esc_mb_id = sql_real_escape_string($mb_id); -$sql = " select mb_id, mb_email_certify2, mb_leave_date, mb_intercept_date from {$g5['member_table']} where mb_id = '{$mb_id}' "; +$sql = " select mb_id, mb_datetime, mb_email_certify2, mb_leave_date, mb_intercept_date from {$g5['member_table']} where mb_id = '{$esc_mb_id}' "; $row = sql_fetch($sql); if (!$row['mb_id']) alert('존재하는 회원이 아닙니다.', G5_URL); @@ -16,21 +17,31 @@ if ( $row['mb_leave_date'] || $row['mb_intercept_date'] ){ alert('탈퇴 또는 차단된 회원입니다.', G5_URL); } -// 인증 링크는 한번만 처리가 되게 한다. -sql_query(" update {$g5['member_table']} set mb_email_certify2 = '' where mb_id = '$mb_id' "); - if ($mb_md5) { - if ($mb_md5 == $row['mb_email_certify2']) + $valid_minutes = isset($config['cf_email_certify_minutes']) ? (int) $config['cf_email_certify_minutes'] : 60; + + if (is_valid_email_certify_token($mb_md5, $row['mb_email_certify2'], $row['mb_datetime'], $valid_minutes)) { - sql_query(" update {$g5['member_table']} set mb_email_certify = '".G5_TIME_YMDHIS."' where mb_id = '{$mb_id}' "); + // 인증 링크는 한번만 처리되도록 인증과 동시에 토큰을 폐기한다. + $esc_mb_md5 = sql_real_escape_string($mb_md5); + sql_query(" update {$g5['member_table']} set mb_email_certify = '".G5_TIME_YMDHIS."', mb_email_certify2 = '' where mb_id = '{$esc_mb_id}' and mb_email_certify2 = '{$esc_mb_md5}' "); + + if (get_sql_affected_rows() <= 0) { + alert('이미 처리되었거나 올바르지 않은 메일인증 요청입니다.', G5_URL); + } alert("메일인증 처리를 완료 하였습니다.\\n\\n지금부터 {$mb_id} 아이디로 로그인 가능합니다.", G5_URL); } else { + if ($mb_md5 === $row['mb_email_certify2']) { + sql_query(" update {$g5['member_table']} set mb_email_certify2 = '' where mb_id = '{$esc_mb_id}' "); + alert('메일인증 유효시간이 만료되었습니다. 인증메일을 다시 요청해 주십시오.', G5_URL); + } + alert('메일인증 요청 정보가 올바르지 않습니다.', G5_URL); } } -alert('제대로 된 값이 넘어오지 않았습니다.', G5_URL); \ No newline at end of file +alert('제대로 된 값이 넘어오지 않았습니다.', G5_URL); diff --git a/bbs/register_email_update.php b/bbs/register_email_update.php index 94251219b..97b47cb22 100644 --- a/bbs/register_email_update.php +++ b/bbs/register_email_update.php @@ -46,7 +46,7 @@ $subject = '['.$config['cf_title'].'] 인증확인 메일입니다.'; $mb_name = $mb['mb_name']; // 어떠한 회원정보도 포함되지 않은 일회용 난수를 생성하여 인증에 사용 (CSPRNG 사용) -$mb_md5 = get_random_token_string(16); +$mb_md5 = get_email_certify_token(); sql_query(" update {$g5['member_table']} set mb_email_certify2 = '$mb_md5' where mb_id = '{$esc_mb_id}' "); @@ -62,4 +62,4 @@ mailer($config['cf_admin_email_name'], $config['cf_admin_email'], $mb_email, $su $sql = " update {$g5['member_table']} set mb_email = '$mb_email' where mb_id = '{$esc_mb_id}' "; sql_query($sql); -alert("인증메일을 {$mb_email} 메일로 다시 보내 드렸습니다.\\n\\n잠시후 {$mb_email} 메일을 확인하여 주십시오.", G5_URL); \ No newline at end of file +alert("인증메일을 {$mb_email} 메일로 다시 보내 드렸습니다.\\n\\n잠시후 {$mb_email} 메일을 확인하여 주십시오.", G5_URL); diff --git a/bbs/register_form_update.php b/bbs/register_form_update.php index 9dd6add28..be3aebcfe 100644 --- a/bbs/register_form_update.php +++ b/bbs/register_form_update.php @@ -325,7 +325,7 @@ if ($w == '') { // 어떠한 회원정보도 포함되지 않은 일회용 난수를 생성하여 인증에 사용 (CSPRNG 사용) if ($config['cf_use_email_certify']) { - $mb_md5 = get_random_token_string(16); + $mb_md5 = get_email_certify_token(); sql_query(" update {$g5['member_table']} set mb_email_certify2 = '$mb_md5' where mb_id = '$mb_id' "); $certify_href = G5_BBS_URL.'/email_certify.php?mb_id='.$mb_id.'&mb_md5='.$mb_md5; } @@ -609,7 +609,7 @@ if ($config['cf_use_email_certify'] && $old_email != $mb_email) { $subject = '['.$config['cf_title'].'] 인증확인 메일입니다.'; // 어떠한 회원정보도 포함되지 않은 일회용 난수를 생성하여 인증에 사용 (CSPRNG 사용) - $mb_md5 = get_random_token_string(16); + $mb_md5 = get_email_certify_token(); sql_query(" update {$g5['member_table']} set mb_email_certify2 = '$mb_md5' where mb_id = '$mb_id' "); diff --git a/bbs/register_form_update_mail1.php b/bbs/register_form_update_mail1.php index e2ec67c28..78e78d5a1 100644 --- a/bbs/register_form_update_mail1.php +++ b/bbs/register_form_update_mail1.php @@ -23,7 +23,8 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가

님의 회원가입을 진심으로 축하합니다.
회원님의 성원에 보답하고자 더욱 더 열심히 하겠습니다.
- 아래의 메일인증을 클릭하시면 회원가입이 완료됩니다.
+ 아래의 메일인증을 클릭하시면 회원가입이 완료됩니다.
+ 인증 링크는 발송 후 분 동안 유효합니다.
감사합니다.

diff --git a/bbs/register_form_update_mail3.php b/bbs/register_form_update_mail3.php index 5ae4d46a9..c9750e678 100644 --- a/bbs/register_form_update_mail3.php +++ b/bbs/register_form_update_mail3.php @@ -26,6 +26,7 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가 아래의 주소를 클릭하시면 인증이 완료됩니다.
+ 인증 링크는 발송 후 분 동안 유효합니다.


회원님의 성원에 보답하고자 더욱 더 열심히 하겠습니다.
diff --git a/install/gnuboard5.sql b/install/gnuboard5.sql index 69a83c219..1027beafd 100644 --- a/install/gnuboard5.sql +++ b/install/gnuboard5.sql @@ -200,6 +200,7 @@ CREATE TABLE IF NOT EXISTS `g5_config` ( `cf_point_term` int(11) NOT NULL DEFAULT '0', `cf_use_copy_log` tinyint(4) NOT NULL DEFAULT '0', `cf_use_email_certify` tinyint(4) NOT NULL DEFAULT '0', + `cf_email_certify_minutes` int(11) NOT NULL DEFAULT '60', `cf_login_point` int(11) NOT NULL DEFAULT '0', `cf_cut_name` tinyint(4) NOT NULL DEFAULT '0', `cf_nick_modify` int(11) NOT NULL DEFAULT '0', @@ -987,4 +988,4 @@ CREATE TABLE IF NOT EXISTS `g5_menu` ( `me_use` tinyint(4) NOT NULL DEFAULT '0', `me_mobile_use` tinyint(4) NOT NULL DEFAULT '0', PRIMARY KEY (`me_id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; \ No newline at end of file +) ENGINE=MyISAM DEFAULT CHARSET=utf8; diff --git a/lib/common.lib.php b/lib/common.lib.php index b41d8ce4b..a9a43d6fa 100644 --- a/lib/common.lib.php +++ b/lib/common.lib.php @@ -2696,6 +2696,54 @@ function get_email_cert_key($mb_id, $mb_datetime) return hash_hmac('sha256', $payload, $key); } +/** + * 발급 시각을 포함한 메일 인증용 일회용 토큰을 생성한다. + * + * @return string + */ +function get_email_certify_token() +{ + return get_random_token_string(16) . '.' . G5_SERVER_TIME; +} + +/** + * 메일 인증 토큰의 일치 여부와 유효시간을 확인한다. + * + * 발급 시각이 없는 기존 토큰은 회원가입 시각을 기준으로 만료 여부를 확인한다. + * + * @param string $token + * @param string $stored_token + * @param string $mb_datetime + * @param int $valid_minutes + * @param int|null $now + * @return bool + */ +function is_valid_email_certify_token($token, $stored_token, $mb_datetime, $valid_minutes, $now = null) +{ + if (!$token || !$stored_token || !hash_equals((string) $stored_token, (string) $token)) { + return false; + } + + $valid_minutes = (int) $valid_minutes; + if ($valid_minutes < 1) { + return true; + } + + if (preg_match('/\.([0-9]{10})$/', $stored_token, $matches)) { + $issued_at = (int) $matches[1]; + } else { + $issued_at = strtotime($mb_datetime); + } + + if (!$issued_at) { + return false; + } + + $now = $now === null ? G5_SERVER_TIME : (int) $now; + + return $issued_at + ($valid_minutes * 60) >= $now; +} + /** * CSRF 방지용 Origin/Referer 검증 (OWASP 권장 패턴). * diff --git a/plugin/social/register_member_update.php b/plugin/social/register_member_update.php index 28736f415..6d624abd7 100644 --- a/plugin/social/register_member_update.php +++ b/plugin/social/register_member_update.php @@ -273,7 +273,7 @@ if($result) { $subject = '['.$config['cf_title'].'] 인증확인 메일입니다.'; // 어떠한 회원정보도 포함되지 않은 일회용 난수를 생성하여 인증에 사용 (CSPRNG 사용) - $mb_md5 = get_random_token_string(16); + $mb_md5 = get_email_certify_token(); sql_query(" update {$g5['member_table']} set mb_email_certify2 = '$mb_md5' where mb_id = '$mb_id' ");