security: KVE-2026-2269 KVE-2026-2331 보안 메일 링크를 고정 도메인으로 제한

검증된 G5_DOMAIN만 인증 및 수신 거부 링크에 사용하고 요청 Host로 대체하지 않는다. 설정 오류 시 인증값과 관련 회원정보를 변경하기 전에 발급을 중단한다.

검증: Host 변조 및 고정 주소 미설정·오류의 격리 메일 검증, PHP 문법 검사 통과.
This commit is contained in:
whitedot
2026-09-09 07:58:16 +00:00
parent 639b788ad1
commit fcc749bde3
10 changed files with 90 additions and 19 deletions
+3 -1
View File
@@ -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 . "<hr size=0><p><span style='font-size:9pt; font-family:굴림'>▶ 더 이상 정보 수신을 원치 않으시면 [<a href='" . G5_BBS_URL . "/email_stop.php?mb_id={$mb_id}&amp;mb_md5={$mb_md5}' target='_blank'>수신거부</a>] 해 주십시오.</span></p>";
$content = $content . "<hr size=0><p><span style='font-size:9pt; font-family:굴림'>▶ 더 이상 정보 수신을 원치 않으시면 [<a href='" . ($security_mail_url . '/' . G5_BBS_DIR) . "/email_stop.php?mb_id={$mb_id}&amp;mb_md5={$mb_md5}' target='_blank'>수신거부</a>] 해 주십시오.</span></p>";
mailer($config['cf_admin_email_name'], $config['cf_admin_email'], $to_email, $subject, $content, 1);
+3 -1
View File
@@ -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 . '<p>더 이상 정보 수신을 원치 않으시면 [<a href="' . G5_BBS_URL . '/email_stop.php?mb_id=' . $mb_id . '&amp;mb_md5=' . $mb_md5 . '" target="_blank">수신거부</a>] 해 주십시오.</p>';
$content = $content . '<p>더 이상 정보 수신을 원치 않으시면 [<a href="' . ($security_mail_url . '/' . G5_BBS_DIR) . '/email_stop.php?mb_id=' . $mb_id . '&amp;mb_md5=' . $mb_md5 . '" target="_blank">수신거부</a>] 해 주십시오.</p>';
mailer($config['cf_title'], $member['mb_email'], $member['mb_email'], $subject, $content, 1);
+8 -2
View File
@@ -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'].'&amp;mb_nonce='.$mb_nonce;
$href = $security_mail_url.'/'.G5_BBS_DIR.'/password_lost_certify.php?mb_no='.$mb['mb_no'].'&amp;mb_nonce='.$mb_nonce;
$subject = "[".$config['cf_title']."] 요청하신 회원정보 찾기 안내 메일입니다.";
@@ -60,7 +66,7 @@ $content .= '<h1 style="padding:30px 30px 0;background:#f7f7f7;color:#555;font-s
$content .= '회원정보 찾기 안내';
$content .= '</h1>';
$content .= '<span style="display:block;padding:10px 30px 30px;background:#f7f7f7;text-align:right">';
$content .= '<a href="'.G5_URL.'" target="_blank">'.$config['cf_title'].'</a>';
$content .= '<a href="'.htmlspecialchars($security_mail_url, ENT_QUOTES, 'UTF-8').'" target="_blank">'.$config['cf_title'].'</a>';
$content .= '</span>';
$content .= '<p style="margin:20px 0 0;padding:30px 30px 30px;border-bottom:1px solid #eee;line-height:1.7em">';
$content .= addslashes($mb['mb_name'])." (".addslashes($mb['mb_nick']).")"." 회원님은 ".G5_TIME_YMDHIS." 에 회원정보 찾기 요청을 하셨습니다.<br>";
+5 -3
View File
@@ -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.'&amp;mb_md5='.$mb_md5;
$certify_href = $security_mail_url.'/'.G5_BBS_DIR.'/email_certify.php?mb_id='.$mb_id.'&amp;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);
+6 -2
View File
@@ -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.'&amp;mb_md5='.$mb_md5;
$certify_href = $security_mail_url.'/'.G5_BBS_DIR.'/email_certify.php?mb_id='.$mb_id.'&amp;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.'&amp;mb_md5='.$mb_md5;
$certify_href = $security_mail_url.'/'.G5_BBS_DIR.'/email_certify.php?mb_id='.$mb_id.'&amp;mb_md5='.$mb_md5;
ob_start();
include_once ('./register_form_update_mail3.php');
+7 -2
View File
@@ -1,6 +1,7 @@
<?php
// 회원가입축하 메일 (회원님께 발송)
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
$mail_site_url = g5_security_mail_base_url();
?>
<!doctype html>
@@ -18,7 +19,9 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
회원가입을 축하합니다.
</h1>
<span style="display:block;padding:10px 30px 30px;background:#f7f7f7;text-align:right">
<a href="<?php echo G5_URL ?>" target="_blank"><?php echo $config['cf_title'] ?></a>
<?php if ($mail_site_url) { ?>
<a href="<?php echo htmlspecialchars($mail_site_url, ENT_QUOTES, 'UTF-8') ?>" target="_blank"><?php echo $config['cf_title'] ?></a>
<?php } else { echo $config['cf_title']; } ?>
</span>
<p style="margin:20px 0 0;padding:30px 30px 50px;min-height:200px;height:auto !important;height:200px;border-bottom:1px solid #eee">
<b><?php echo $mb_name ?></b> 님의 회원가입을 진심으로 축하합니다.<br>
@@ -31,7 +34,9 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
<?php if ($config['cf_use_email_certify']) { ?>
<a href="<?php echo $certify_href ?>" target="_blank" style="display:block;padding:30px 0;background:#484848;color:#fff;text-decoration:none;text-align:center">메일인증</a>
<?php } else { ?>
<a href="<?php echo G5_URL ?>" target="_blank" style="display:block;padding:30px 0;background:#484848;color:#fff;text-decoration:none;text-align:center">사이트바로가기</a>
<?php if ($mail_site_url) { ?>
<a href="<?php echo htmlspecialchars($mail_site_url, ENT_QUOTES, 'UTF-8') ?>" target="_blank" style="display:block;padding:30px 0;background:#484848;color:#fff;text-decoration:none;text-align:center">사이트바로가기</a>
<?php } ?>
<?php } ?>
</div>
</div>
+4 -1
View File
@@ -1,6 +1,7 @@
<?php
// 회원가입 메일 (관리자 메일로 발송)
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
$mail_site_url = g5_security_mail_base_url();
?>
<!doctype html>
@@ -18,7 +19,9 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
회원가입 알림 메일
</h1>
<span style="display:block;padding:10px 30px 30px;background:#f7f7f7;text-align:right">
<a href="<?php echo G5_URL ?>" target="_blank"><?php echo $config['cf_title'] ?></a>
<?php if ($mail_site_url) { ?>
<a href="<?php echo htmlspecialchars($mail_site_url, ENT_QUOTES, 'UTF-8') ?>" target="_blank"><?php echo $config['cf_title'] ?></a>
<?php } else { echo $config['cf_title']; } ?>
</span>
<p style="margin:20px 0 0;padding:30px 30px 50px;min-height:200px;height:auto !important;height:200px;border-bottom:1px solid #eee">
<b><?php echo $mb_name ?></b> 님께서 회원가입 하셨습니다.<br>
+4 -1
View File
@@ -1,6 +1,7 @@
<?php
// E-mail 수정시 인증 메일 (회원님께 발송)
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
$mail_site_url = g5_security_mail_base_url();
?>
<!doctype html>
@@ -18,7 +19,9 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
회원 인증 메일입니다.
</h1>
<span style="display:block;padding:10px 30px 30px;background:#f7f7f7;text-align:right">
<a href="<?php echo G5_URL ?>" target="_blank"><?php echo $config['cf_title'] ?></a>
<?php if ($mail_site_url) { ?>
<a href="<?php echo htmlspecialchars($mail_site_url, ENT_QUOTES, 'UTF-8') ?>" target="_blank"><?php echo $config['cf_title'] ?></a>
<?php } else { echo $config['cf_title']; } ?>
</span>
<p style="margin:20px 0 0;padding:30px 30px 50px;min-height:200px;height:auto !important;height:200px;border-bottom:1px solid #eee">
<?php if($w == 'u') { ?>
+47 -5
View File
@@ -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;
+3 -1
View File
@@ -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.'&amp;mb_md5='.$mb_md5;
$certify_href = $security_mail_url.'/'.G5_BBS_DIR.'/email_certify.php?mb_id='.$mb_id.'&amp;mb_md5='.$mb_md5;
ob_start();
include_once (G5_BBS_PATH.'/register_form_update_mail3.php');