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' ");