[보안/SER-004] 인증 없는 휴면계정 해제 차단 + 메일토큰 난수 강화

웹취약점 점검(SER-004)에서 확인된 휴면계정 관련 취약점 2건 조치.

1) 인증 없는 휴면계정 해제 (CWE-306, 긴급)
   - actionDiapause.do 가 요청 파라미터 mber_id 만으로 즉시 deleteDiapause 실행 →
     아이디만 알면 제3자가 임의 계정의 휴면을 해제할 수 있었음(로컬 PoC 실증).
   - 파라미터를 신뢰하지 않고, ID/PW 인증 성공 시에만 세션에 적재되는 diapauseVO 의
     mber_id 로만 해제. 세션 부재 시 resCode=002 거부, 성공 시 1회성 소거.
   - 정상 경로 releaseDiapauseAjax(토큰 검증)와 동일 원칙. 정상 사용자는 원래도
     세션값(${sessionScope.diapauseVO.mber_id})을 보냈으므로 결과 동일(회귀 없음).
   - mbl/EgovDiapause.jsp: resCode 분기로 거부 시 재로그인 안내.

2) 예측 가능한 난수 (CWE-330, 중)
   - 휴면 활성화 메일 토큰(20자) 생성을 java.util.Random → SecureRandom 으로 교체.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbEjijyQdS54xso1d2ks3u
This commit is contained in:
infraurp 2026-08-31 10:31:08 +09:00
parent 722fd978ce
commit a085956a8b
2 changed files with 32 additions and 9 deletions

View File

@ -3,7 +3,7 @@ package egovframework.com.uat.uia.web;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Random; import java.security.SecureRandom;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -441,7 +441,9 @@ public class EgovLoginController {
char[] charaters = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9'}; char[] charaters = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','0','1','2','3','4','5','6','7','8','9'};
StringBuffer sb = new StringBuffer(); StringBuffer sb = new StringBuffer();
Random rn = new Random(); // [보안/SER-004] 휴면 활성화 메일 토큰 난수를 예측 가능한 java.util.Random 에서
// 암호학적 안전 난수(SecureRandom)로 교체(CWE-330). 토큰 재현·추측 방지.
SecureRandom rn = new SecureRandom();
for( int i = 0 ; i < 20 ; i++ ){ for( int i = 0 ; i < 20 ; i++ ){
sb.append( charaters[ rn.nextInt( charaters.length ) ] ); sb.append( charaters[ rn.nextInt( charaters.length ) ] );
} }
@ -461,14 +463,30 @@ public class EgovLoginController {
/** /**
* 휴면계정 활성화 * 휴면계정 활성화
*
* [보안/SER-004] 인증 없는 휴면계정 해제 차단.
* 종전: 요청 파라미터 mber_id 만으로 즉시 deleteDiapause 실행 → 아이디만 알면
* 제3자가 임의 계정의 휴면을 해제할 수 있었다(본인확인 부재, CWE-306).
* 변경: 파라미터를 신뢰하지 않고, ID/PW 인증에 성공해야만 세션에 적재되는
* diapauseVO(EgovSpringSecurityLoginFilter/diapauseLogin 에서 setAttribute)의
* mber_id 로만 해제한다. 프론트 경로(releaseDiapauseAjax)가 이메일 토큰을
* 검증하는 것과 동일하게, 모바일 경로는 '로그인 세션' 을 본인확인 근거로 삼는다.
* 세션 부재·불일치 시 해제하지 않고 resCode=002 로 거부, 성공 시 1회성 소거.
*/ */
@RequestMapping(value = "/uat/uia/actionDiapause.do") @RequestMapping(value = "/uat/uia/actionDiapause.do")
public String actionDiapause(HttpServletRequest request, HttpServletResponse response public String actionDiapause(HttpServletRequest request, HttpServletResponse response
,@RequestParam(value="mber_id", required=true) String mber_id
,ModelMap model) throws Exception { ,ModelMap model) throws Exception {
LoginVO diapauseVO = (LoginVO) request.getSession().getAttribute("diapauseVO");
if (diapauseVO == null || diapauseVO.getMber_id() == null || "".equals(diapauseVO.getMber_id())) {
// 로그인(본인확인) 없이 직접 호출된 요청 — 해제 거부
model.addAttribute("resCode", "002");
return JSON;
}
LoginVO vo = new LoginVO(); LoginVO vo = new LoginVO();
vo.setMber_id(mber_id); vo.setMber_id(diapauseVO.getMber_id());
loginService.deleteDiapause(vo); loginService.deleteDiapause(vo);
request.getSession().removeAttribute("diapauseVO"); // 재사용 방지
model.addAttribute("resCode", "001");
return JSON; return JSON;
} }

View File

@ -26,14 +26,19 @@ function actionDiapause() {
$.ajax({ $.ajax({
url : "/uat/uia/actionDiapause.do", url : "/uat/uia/actionDiapause.do",
type : "POST", type : "POST",
data : {"mber_id" : $("#mber_id").val(), "esntl_id" : $("#esntl_id").val()},
dataType : "json", dataType : "json",
success:function(object){ success:function(object){
alert("휴먼계정이 해제 되었습니다. \n다시 로그인 하세요."); // [보안] 서버가 로그인 세션(diapauseVO)으로 본인확인 후 해제한다.
// resCode 001=성공, 002=본인확인 실패(세션 만료 등) → 재로그인 유도.
if(object && object.resCode === "001"){
alert("휴면계정이 해제 되었습니다. \n다시 로그인 하세요.");
} else {
alert("본인확인이 만료되었습니다. \n다시 로그인 후 시도해 주세요.");
}
location.href = "/uat/uia/egovLoginUsr.do"; location.href = "/uat/uia/egovLoginUsr.do";
}, },
error:function(xhr,status,error){alert(status);} error:function(xhr,status,error){alert(status);}
}); });
} }
//--> //-->
</script> </script>