From dccb50d8a3d031fcc49853b544fa3464516eff6d Mon Sep 17 00:00:00 2001 From: thisgun Date: Thu, 16 Apr 2026 02:54:05 +0000 Subject: [PATCH] =?UTF-8?q?[security]=ED=9A=8C=EC=9B=90=20ID/=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=97=B4=EA=B1=B0=20=EA=B3=B5=EA=B2=A9=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bbs/ajax.mb_id.php | 5 +++++ bbs/ajax.mb_nick.php | 5 +++++ bbs/ajax.mb_recommend.php | 5 +++++ bbs/password_lost2.php | 21 ++++++++++++++------- lib/common.lib.php | 28 ++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/bbs/ajax.mb_id.php b/bbs/ajax.mb_id.php index 85498ae87..bbbc7610e 100644 --- a/bbs/ajax.mb_id.php +++ b/bbs/ajax.mb_id.php @@ -2,6 +2,11 @@ include_once('./_common.php'); include_once(G5_LIB_PATH.'/register.lib.php'); +// 회원 ID 열거 공격 방지를 위한 Rate Limit (분당 30회) +if (function_exists('check_rate_limit') && !check_rate_limit('ajax_mb_id_check', 30, 60)) { + die('너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해 주세요.'); +} + $mb_id = isset($_POST['reg_mb_id']) ? trim($_POST['reg_mb_id']) : ''; set_session('ss_check_mb_id', ''); diff --git a/bbs/ajax.mb_nick.php b/bbs/ajax.mb_nick.php index 1bfe08642..dbd55e171 100644 --- a/bbs/ajax.mb_nick.php +++ b/bbs/ajax.mb_nick.php @@ -2,6 +2,11 @@ include_once('./_common.php'); include_once(G5_LIB_PATH.'/register.lib.php'); +// 회원 닉네임 열거 공격 방지를 위한 Rate Limit (분당 30회) +if (function_exists('check_rate_limit') && !check_rate_limit('ajax_mb_nick_check', 30, 60)) { + die('너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해 주세요.'); +} + $mb_nick = isset($_POST['reg_mb_nick']) ? trim($_POST['reg_mb_nick']) : ''; $mb_id = isset($_POST['reg_mb_id']) ? trim($_POST['reg_mb_id']) : ''; diff --git a/bbs/ajax.mb_recommend.php b/bbs/ajax.mb_recommend.php index c42af0c6e..1ed9a499b 100644 --- a/bbs/ajax.mb_recommend.php +++ b/bbs/ajax.mb_recommend.php @@ -2,6 +2,11 @@ include_once("./_common.php"); include_once(G5_LIB_PATH."/register.lib.php"); +// 추천인 ID 열거 공격 방지를 위한 Rate Limit (분당 30회) +if (function_exists('check_rate_limit') && !check_rate_limit('ajax_mb_recommend_check', 30, 60)) { + die('너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해 주세요.'); +} + $mb_recommend = isset($_POST["reg_mb_recommend"]) ? trim($_POST["reg_mb_recommend"]) : ''; if ($msg = valid_mb_id($mb_recommend)) { diff --git a/bbs/password_lost2.php b/bbs/password_lost2.php index 912ada60b..213f55a3b 100644 --- a/bbs/password_lost2.php +++ b/bbs/password_lost2.php @@ -16,17 +16,24 @@ $email = get_email_address(trim($_POST['mb_email'])); if (!$email) alert_close('메일주소 오류입니다.'); +// OWASP 권장: 이메일 존재 여부와 무관하게 동일한 응답 메시지 사용 +// (이메일 열거 공격 방지) +$generic_message = $email.' 메일로 회원아이디와 비밀번호를 인증할 수 있는 메일이 발송 되었습니다.\\n\\n메일을 확인하여 주십시오.'; + $sql = " select count(*) as cnt from {$g5['member_table']} where mb_email = '$email' "; $row = sql_fetch($sql); -if ($row['cnt'] > 1) - alert('동일한 메일주소가 2개 이상 존재합니다.\\n\\n관리자에게 문의하여 주십시오.'); +if ($row['cnt'] > 1) { + // 시스템 데이터 무결성 이슈 - 운영자 로그에만 기록하고 사용자에겐 일반 메시지 + @error_log("[g5 password_lost2] Duplicate email detected: $email (count={$row['cnt']})"); + alert_close($generic_message); +} $sql = " select mb_no, mb_id, mb_name, mb_nick, mb_email, mb_datetime, mb_leave_date from {$g5['member_table']} where mb_email = '$email' "; $mb = sql_fetch($sql); -if (empty($mb['mb_id']) || $mb['mb_leave_date']) { - alert('존재하지 않는 회원입니다.'); -} elseif (is_admin($mb['mb_id'])) { - alert('관리자 아이디는 접근 불가합니다.'); + +// 회원이 없거나 탈퇴했거나 관리자이면 메일 발송 없이 동일한 메시지로 응답 +if (empty($mb['mb_id']) || $mb['mb_leave_date'] || is_admin($mb['mb_id'])) { + alert_close($generic_message); } // 임시비밀번호 발급 @@ -74,4 +81,4 @@ mailer($config['cf_admin_email_name'], $config['cf_admin_email'], $mb['mb_email' run_event('password_lost2_after', $mb, $mb_nonce, $mb_lost_certify); -alert_close($email.' 메일로 회원아이디와 비밀번호를 인증할 수 있는 메일이 발송 되었습니다.\\n\\n메일을 확인하여 주십시오.'); \ No newline at end of file +alert_close($generic_message); \ No newline at end of file diff --git a/lib/common.lib.php b/lib/common.lib.php index af631755d..f7510a7af 100644 --- a/lib/common.lib.php +++ b/lib/common.lib.php @@ -2531,6 +2531,34 @@ function check_token() return true; } +/** + * 세션 기반 단순 Rate Limit. 자동화 도구의 무차별 호출을 늦추기 위한 용도. + * + * 같은 키에 대해 $window 초 동안 $max 회까지만 허용. 초과 시 false 반환. + * 세션 쿠키를 무시하는 정교한 봇은 우회 가능하지만, 세션 생성 비용으로 attack rate가 떨어지고 + * 가장 흔한 form-only enumeration 시나리오는 효과적으로 차단된다. + * + * @param string $key 레이트 리밋 식별자 (예: 'ajax_mb_id_check') + * @param int $max 윈도우당 최대 허용 횟수 + * @param int $window 윈도우 크기 (초) + * @return bool true = 허용, false = 차단 + */ +function check_rate_limit($key, $max = 30, $window = 60) +{ + $session_key = 'ss_rate_' . $key; + $now = time(); + + $data = get_session($session_key); + if (!is_array($data) || !isset($data['reset']) || $data['reset'] < $now) { + $data = array('count' => 0, 'reset' => $now + $window); + } + + $data['count']++; + set_session($session_key, $data); + + return $data['count'] <= $max; +} + /** * 이메일 미인증 회원의 메일주소 변경 페이지 접근 토큰을 생성한다. *