프로세스 검증 누락 (PV) | 위험도: 4 (높음) | 통제구분: 5.4.2 거래 무결성
인터넷뱅킹 계좌이체 시스템
받는 계좌, 금액, 내용 입력
입력 내용 검토 및 확인
보안카드 또는 OTP 번호 입력
최종 이체 처리
// ❌ 프로세스 검증 누락
@PostMapping("/transfer/execute")
public String executeTransfer(
@RequestParam String toAccount,
@RequestParam int amount) {
// 이전 단계 완료 여부 확인 안 함!
// OTP 검증 안 함!
// 세션 토큰 검증 안 함!
Transfer transfer = new Transfer();
transfer.setToAccount(toAccount);
transfer.setAmount(amount);
// 바로 이체 실행
transferService.execute(transfer);
return "success";
}
공격 시나리오를 선택하세요
// ✅ 프로세스 검증 구현
// 1단계: 이체 정보 입력
@PostMapping("/transfer/step1")
public String inputTransfer(Transfer transfer,
HttpSession session) {
// 프로세스 토큰 생성
String processToken = UUID.randomUUID().toString();
session.setAttribute("processToken", processToken);
session.setAttribute("step1Data", transfer);
session.setAttribute("currentStep", 1);
return "redirect:/transfer/step2";
}
// 2단계: 이체 정보 확인
@PostMapping("/transfer/step2")
public String confirmTransfer(HttpSession session) {
// Step 1 완료 여부 검증
if (session.getAttribute("currentStep") == null ||
(int)session.getAttribute("currentStep") != 1) {
throw new InvalidProcessException();
}
// OTP 토큰 생성 및 SMS 발송
String otpToken = generateOTP();
session.setAttribute("otpToken", otpToken);
session.setAttribute("otpExpiry",
System.currentTimeMillis() + 180000); // 3분
sendSMS(otpToken);
session.setAttribute("currentStep", 2);
return "redirect:/transfer/step3";
}
// 3단계: OTP 인증
@PostMapping("/transfer/step3")
public String verifyOTP(String inputOTP,
HttpSession session) {
// Step 2 완료 여부 검증
if ((int)session.getAttribute("currentStep") != 2) {
throw new InvalidProcessException();
}
// OTP 검증
String savedOTP = (String)session.getAttribute("otpToken");
long expiry = (long)session.getAttribute("otpExpiry");
if (System.currentTimeMillis() > expiry) {
throw new OTPExpiredException();
}
if (!inputOTP.equals(savedOTP)) {
throw new InvalidOTPException();
}
// 최종 실행 토큰 생성 (일회용)
String executeToken = UUID.randomUUID().toString();
session.setAttribute("executeToken", executeToken);
session.setAttribute("currentStep", 3);
return "redirect:/transfer/execute";
}
// 4단계: 이체 실행
@PostMapping("/transfer/execute")
public String executeTransfer(String executeToken,
HttpSession session) {
// Step 3 완료 여부 검증
if ((int)session.getAttribute("currentStep") != 3) {
throw new InvalidProcessException();
}
// 실행 토큰 검증 (일회용)
String savedToken = (String)session.getAttribute("executeToken");
if (!executeToken.equals(savedToken)) {
throw new InvalidTokenException();
}
// 토큰 즉시 삭제 (재사용 방지)
session.removeAttribute("executeToken");
// 프로세스 토큰 검증
String processToken = (String)session.getAttribute("processToken");
if (processToken == null) {
throw new InvalidProcessException();
}
// 이체 실행
Transfer transfer = (Transfer)session.getAttribute("step1Data");
transferService.execute(transfer);
// 모든 세션 데이터 삭제
session.removeAttribute("processToken");
session.removeAttribute("step1Data");
session.removeAttribute("currentStep");
return "success";
}