From fcc749bde34cb56b171727eb1db660140e9764bd Mon Sep 17 00:00:00 2001 From: whitedot Date: Tue, 8 Sep 2026 08:32:14 +0000 Subject: [PATCH] =?UTF-8?q?security:=20KVE-2026-2269=20KVE-2026-2331=20?= =?UTF-8?q?=EB=B3=B4=EC=95=88=20=EB=A9=94=EC=9D=BC=20=EB=A7=81=ED=81=AC?= =?UTF-8?q?=EB=A5=BC=20=EA=B3=A0=EC=A0=95=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 검증된 G5_DOMAIN만 인증 및 수신 거부 링크에 사용하고 요청 Host로 대체하지 않는다. 설정 오류 시 인증값과 관련 회원정보를 변경하기 전에 발급을 중단한다. 검증: Host 변조 및 고정 주소 미설정·오류의 격리 메일 검증, PHP 문법 검사 통과. --- adm/mail_select_update.php | 4 +- adm/mail_test.php | 4 +- bbs/password_lost2.php | 10 ++++- bbs/register_email_update.php | 8 ++-- bbs/register_form_update.php | 8 +++- bbs/register_form_update_mail1.php | 9 +++- bbs/register_form_update_mail2.php | 5 ++- bbs/register_form_update_mail3.php | 5 ++- lib/common.lib.php | 52 +++++++++++++++++++++--- plugin/social/register_member_update.php | 4 +- 10 files changed, 90 insertions(+), 19 deletions(-) diff --git a/adm/mail_select_update.php b/adm/mail_select_update.php index c0d7c7400..c6083d90d 100644 --- a/adm/mail_select_update.php +++ b/adm/mail_select_update.php @@ -8,6 +8,8 @@ $html_title = '회원메일 발송'; check_demo(); +$security_mail_url = g5_require_security_mail_url(); + check_admin_token(); require_once './admin.head.php'; @@ -59,7 +61,7 @@ for ($i = 0; $i < count($member_list); $i++) { $content = preg_replace("/{회원아이디}/", $mb_id, (string)$content); $content = preg_replace("/{이메일}/", $to_email, (string)$content); - $content = $content . "

▶ 더 이상 정보 수신을 원치 않으시면 [수신거부] 해 주십시오.

"; + $content = $content . "

▶ 더 이상 정보 수신을 원치 않으시면 [수신거부] 해 주십시오.

"; mailer($config['cf_admin_email_name'], $config['cf_admin_email'], $to_email, $subject, $content, 1); diff --git a/adm/mail_test.php b/adm/mail_test.php index 13d74aeca..afb8a8fd8 100644 --- a/adm/mail_test.php +++ b/adm/mail_test.php @@ -12,6 +12,8 @@ auth_check_menu($auth, $sub_menu, 'w'); check_demo(); +$security_mail_url = g5_require_security_mail_url(); + $g5['title'] = '회원메일 테스트'; $name = get_text($member['mb_name']); @@ -33,7 +35,7 @@ $content = preg_replace("/{이메일}/", $email, (string)$content); $mb_md5 = md5($member['mb_id'] . $member['mb_email'] . $member['mb_datetime']); -$content = $content . '

더 이상 정보 수신을 원치 않으시면 [수신거부] 해 주십시오.

'; +$content = $content . '

더 이상 정보 수신을 원치 않으시면 [수신거부] 해 주십시오.

'; mailer($config['cf_title'], $member['mb_email'], $member['mb_email'], $subject, $content, 1); diff --git a/bbs/password_lost2.php b/bbs/password_lost2.php index 04b629a2c..1df835d34 100644 --- a/bbs/password_lost2.php +++ b/bbs/password_lost2.php @@ -20,6 +20,12 @@ if (!$email) // (이메일 열거 공격 방지) $generic_message = $email.' 메일로 회원아이디와 비밀번호를 인증할 수 있는 메일이 발송 되었습니다.\\n\\n메일을 확인하여 주십시오.'; +$security_mail_url = g5_security_mail_base_url(); +if ($security_mail_url === false) { + error_log('[g5 security mail] Valid G5_DOMAIN is required.'); + alert_close($generic_message); +} + $sql = " select count(*) as cnt from {$g5['member_table']} where mb_email = '$email' "; $row = sql_fetch($sql); if ($row['cnt'] > 1) { @@ -48,7 +54,7 @@ $sql = " update {$g5['member_table']} set mb_lost_certify = '$mb_nonce $mb_lost_ sql_query($sql); // 인증 링크 생성 -$href = G5_BBS_URL.'/password_lost_certify.php?mb_no='.$mb['mb_no'].'&mb_nonce='.$mb_nonce; +$href = $security_mail_url.'/'.G5_BBS_DIR.'/password_lost_certify.php?mb_no='.$mb['mb_no'].'&mb_nonce='.$mb_nonce; $subject = "[".$config['cf_title']."] 요청하신 회원정보 찾기 안내 메일입니다."; @@ -60,7 +66,7 @@ $content .= '

'; -$content .= ''.$config['cf_title'].''; +$content .= ''.$config['cf_title'].''; $content .= ''; $content .= '

'; $content .= addslashes($mb['mb_name'])." (".addslashes($mb['mb_nick']).")"." 회원님은 ".G5_TIME_YMDHIS." 에 회원정보 찾기 요청을 하셨습니다.
"; diff --git a/bbs/register_email_update.php b/bbs/register_email_update.php index 97b47cb22..0c4963c0d 100644 --- a/bbs/register_email_update.php +++ b/bbs/register_email_update.php @@ -40,17 +40,19 @@ if ($row['cnt']) { alert("{$mb_email} 메일은 이미 존재하는 메일주소 입니다.\\n\\n다른 메일주소를 입력해 주십시오."); } +$security_mail_url = g5_require_security_mail_url(); + // 인증메일 발송 $subject = '['.$config['cf_title'].'] 인증확인 메일입니다.'; $mb_name = $mb['mb_name']; // 어떠한 회원정보도 포함되지 않은 일회용 난수를 생성하여 인증에 사용 (CSPRNG 사용) -$mb_md5 = get_email_certify_token(); +$mb_md5 = get_email_certify_token(); sql_query(" update {$g5['member_table']} set mb_email_certify2 = '$mb_md5' where mb_id = '{$esc_mb_id}' "); -$certify_href = G5_BBS_URL.'/email_certify.php?mb_id='.$mb_id.'&mb_md5='.$mb_md5; +$certify_href = $security_mail_url.'/'.G5_BBS_DIR.'/email_certify.php?mb_id='.$mb_id.'&mb_md5='.$mb_md5; ob_start(); include_once ('./register_form_update_mail3.php'); @@ -62,4 +64,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); +alert("인증메일을 {$mb_email} 메일로 다시 보내 드렸습니다.\\n\\n잠시후 {$mb_email} 메일을 확인하여 주십시오.", G5_URL); diff --git a/bbs/register_form_update.php b/bbs/register_form_update.php index be3aebcfe..b27cf3cb2 100644 --- a/bbs/register_form_update.php +++ b/bbs/register_form_update.php @@ -65,6 +65,10 @@ $mb_9 = isset($_POST['mb_9']) ? trim($_POST['mb_9']) $mb_10 = isset($_POST['mb_10']) ? trim($_POST['mb_10']) : ""; $mb_name = addslashes(clean_xss_tags(stripslashes($mb_name), 1, 1)); $mb_email = get_email_address($mb_email); +$security_mail_url = g5_security_mail_base_url(); +if ($config['cf_use_email_certify'] && ($w == '' || $member['mb_email'] != $mb_email)) { + $security_mail_url = g5_require_security_mail_url(); +} $mb_homepage = addslashes(clean_xss_tags(stripslashes($mb_homepage), 1, 1)); $mb_tel = addslashes(clean_xss_tags(stripslashes($mb_tel), 1, 1)); $mb_zip1 = preg_replace('/[^0-9]/', '', $mb_zip1); @@ -327,7 +331,7 @@ if ($w == '') { if ($config['cf_use_email_certify']) { $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; + $certify_href = $security_mail_url.'/'.G5_BBS_DIR.'/email_certify.php?mb_id='.$mb_id.'&mb_md5='.$mb_md5; } ob_start(); @@ -613,7 +617,7 @@ if ($config['cf_use_email_certify'] && $old_email != $mb_email) { 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; + $certify_href = $security_mail_url.'/'.G5_BBS_DIR.'/email_certify.php?mb_id='.$mb_id.'&mb_md5='.$mb_md5; ob_start(); include_once ('./register_form_update_mail3.php'); diff --git a/bbs/register_form_update_mail1.php b/bbs/register_form_update_mail1.php index 78e78d5a1..8d59e889c 100644 --- a/bbs/register_form_update_mail1.php +++ b/bbs/register_form_update_mail1.php @@ -1,6 +1,7 @@ @@ -18,7 +19,9 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가 회원가입을 축하합니다.

- + + +

님의 회원가입을 진심으로 축하합니다.
@@ -31,7 +34,9 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가 메일인증 - 사이트바로가기 + + 사이트바로가기 + diff --git a/bbs/register_form_update_mail2.php b/bbs/register_form_update_mail2.php index f30b7b9e4..55b17679a 100644 --- a/bbs/register_form_update_mail2.php +++ b/bbs/register_form_update_mail2.php @@ -1,6 +1,7 @@ @@ -18,7 +19,9 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가 회원가입 알림 메일 - + + +

님께서 회원가입 하셨습니다.
diff --git a/bbs/register_form_update_mail3.php b/bbs/register_form_update_mail3.php index c9750e678..2a0913c73 100644 --- a/bbs/register_form_update_mail3.php +++ b/bbs/register_form_update_mail3.php @@ -1,6 +1,7 @@ @@ -18,7 +19,9 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가 회원 인증 메일입니다. - + + +

diff --git a/lib/common.lib.php b/lib/common.lib.php index 17fec1a85..9ca2dc112 100644 --- a/lib/common.lib.php +++ b/lib/common.lib.php @@ -2696,11 +2696,53 @@ function get_email_cert_key($mb_id, $mb_datetime) return hash_hmac('sha256', $payload, $key); } -/** - * 발급 시각을 포함한 메일 인증용 일회용 토큰을 생성한다. - * - * @return string - */ +// 보안 메일은 요청 헤더가 아닌 운영자가 지정한 공개 URL만 사용한다. +function g5_security_mail_base_url($domain = null) +{ + if ($domain === null) { + $domain = defined('G5_DOMAIN') ? G5_DOMAIN : ''; + } + if (!is_string($domain) || $domain === '' || preg_match('/[\x00-\x20\x7f\\\\<>"\']/', $domain)) { + return false; + } + $parts = @parse_url($domain); + if (!$parts || empty($parts['scheme']) || !in_array(strtolower($parts['scheme']), array('http', 'https'), true) + || empty($parts['host']) || isset($parts['user']) || isset($parts['pass']) + || isset($parts['query']) || isset($parts['fragment'])) { + return false; + } + // 호스트에는 포트 구분용 IPv6 대괄호 외의 URL 구문을 허용하지 않는다. + $host = $parts['host']; + if ($host[0] === '[') { + if (substr($host, -1) !== ']' || !filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) return false; + } elseif (!preg_match('/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9.])?$/i', $host)) { + return false; + } + if ($host[0] !== '[') { + if (strlen($host) > 254) return false; + $hostname = substr($host, -1) === '.' ? substr($host, 0, -1) : $host; + foreach (explode('.', $hostname) as $label) { + if (!preg_match('/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i', $label)) return false; + } + } + if (isset($parts['port']) && ($parts['port'] < 1 || $parts['port'] > 65535)) return false; + if (isset($parts['path']) && (preg_match('/%(?:0[0-9a-f]|1[0-9a-f]|7f|2f|5c)/i', $parts['path']) + || preg_match('~(?:^|/)(?:\.|%2e){1,2}(?:/|$)~i', $parts['path']))) return false; + + return rtrim($domain, '/'); +} + +function g5_require_security_mail_url() +{ + $url = g5_security_mail_base_url(); + if ($url === false) { + error_log('[g5 security mail] Valid G5_DOMAIN is required.'); + alert('메일 인증을 위한 사이트 주소가 설정되지 않았습니다. 사이트 관리자에게 문의해 주십시오.'); + } + return $url; +} + +// 발급 시각을 포함한 메일 인증용 일회용 토큰을 생성한다. function get_email_certify_token() { return get_random_token_string(16) . '.' . G5_SERVER_TIME; diff --git a/plugin/social/register_member_update.php b/plugin/social/register_member_update.php index 6d624abd7..eb7c8dbf7 100644 --- a/plugin/social/register_member_update.php +++ b/plugin/social/register_member_update.php @@ -86,10 +86,12 @@ $data = array( 'mb_name' => $mb_name, ); +$security_mail_url = g5_security_mail_base_url(); $mb_email_certify = G5_TIME_YMDHIS; //메일인증을 사용한다면 if( defined('G5_SOCIAL_CERTIFY_MAIL') && G5_SOCIAL_CERTIFY_MAIL && $config['cf_use_email_certify'] ){ + $security_mail_url = g5_require_security_mail_url(); $mb_email_certify = ''; } @@ -277,7 +279,7 @@ if($result) { 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; + $certify_href = $security_mail_url.'/'.G5_BBS_DIR.'/email_certify.php?mb_id='.$mb_id.'&mb_md5='.$mb_md5; ob_start(); include_once (G5_BBS_PATH.'/register_form_update_mail3.php');