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

크로스사이트 요청변조 (CSRF) | 위험도: 4 (높음) | 통제구분: 5.8.4 서비스 보호

XX은행

인터넷뱅킹 - 로그인 상태

홍길동님 로그인 중 취약점: CSRF 토큰 없음

내 계좌

입출금 통장

110-123-456789

잔액: 5,000,000
악성 피싱 사이트
🎁

축하합니다! 100만원 경품 당첨!

아래 버튼을 클릭하여 경품을 수령하세요

CSRF 공격 시나리오

취약한 Java 코드

위험
// ❌ 취약한 코드 - CSRF 토큰 검증 없음
@PostMapping("/transfer")
public String transfer(
    @RequestParam String toAccount,
    @RequestParam Long amount,
    HttpSession session) {
    
    // 세션만 확인 (CSRF 토큰 없음!)
    User user = (User) session.getAttribute("user");
    if (user == null) {
        return "redirect:/login";
    }
    
    // CSRF 토큰 검증 없이 바로 이체 실행
    bankService.transfer(
        user.getAccount(),
        toAccount,
        amount
    );
    
    return "transfer_success";
}

은행 서버 로그

SECURE
[SERVER] Banking system ready...
[SESSION] User 홍길동 logged in

안전한 Java 코드

권장
// ✅ 안전한 코드 - CSRF 토큰 검증
@PostMapping("/transfer")
public String transfer(
    @RequestParam String toAccount,
    @RequestParam Long amount,
    @RequestParam String csrfToken,
    HttpSession session) {
    
    // 1. 세션 확인
    User user = (User) session.getAttribute("user");
    if (user == null) {
        return "redirect:/login";
    }
    
    // 2. CSRF 토큰 검증 (필수!)
    String sessionToken = 
        (String) session.getAttribute("csrfToken");
    
    if (sessionToken == null || 
        !sessionToken.equals(csrfToken)) {
        throw new SecurityException(
            "CSRF token validation failed"
        );
    }
    
    // 3. 추가 검증: Referer 확인
    String referer = request.getHeader("Referer");
    if (referer == null || 
        !referer.startsWith("https://xxbank.com")) {
        throw new SecurityException(
            "Invalid referer"
        );
    }
    
    // 4. 이체 실행
    bankService.transfer(
        user.getAccount(),
        toAccount,
        amount
    );
    
    // 5. 토큰 재생성 (One-time use)
    String newToken = generateCSRFToken();
    session.setAttribute("csrfToken", newToken);
    
    return "transfer_success";
}

// CSRF 토큰 생성
private String generateCSRFToken() {
    return UUID.randomUUID().toString();
}

// Spring Security 설정
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(
            HttpSecurity http) throws Exception {
        
        http
            // CSRF 보호 활성화
            .csrf(csrf -> csrf
                .csrfTokenRepository(
                    CookieCsrfTokenRepository
                        .withHttpOnlyFalse()
                )
            )
            
            // SameSite 쿠키 설정
            .sessionManagement(session -> session
                .sessionCreationPolicy(
                    SessionCreationPolicy.IF_REQUIRED
                )
            );
        
        return http.build();
    }
}

// JSP/Thymeleaf에서 토큰 사용
<!-- JSP -->
<form action="/transfer" method="post">
    <input type="hidden" 
           name="csrfToken" 
           value="${csrfToken}"/>
    <!-- 폼 필드 -->
</form>

<!-- Thymeleaf -->
<form th:action="@{/transfer}" method="post">
    <input type="hidden" 
           th:name="${_csrf.parameterName}" 
           th:value="${_csrf.token}"/>
    <!-- 폼 필드 -->
</form>

// Double Submit Cookie 패턴
@PostMapping("/transfer")
public String transfer(
    @CookieValue("XSRF-TOKEN") String cookieToken,
    @RequestParam("_csrf") String formToken,
    HttpSession session) {
    
    // 쿠키 토큰과 폼 토큰 비교
    if (!cookieToken.equals(formToken)) {
        throw new SecurityException(
            "CSRF token mismatch"
        );
    }
    
    // 계속 처리...
}

CSRF 방어 체크리스트

CSRF 토큰: 모든 상태 변경 요청에 토큰 검증
SameSite 쿠키: Strict 또는 Lax 설정
Referer 검증: 요청 출처 확인
토큰 재생성: 사용 후 토큰 무효화
Custom Header: X-Requested-With 확인