크로스사이트 요청변조 (CSRF) | 위험도: 4 (높음) | 통제구분: 5.8.4 서비스 보호
인터넷뱅킹 - 로그인 상태
입출금 통장
110-123-456789
아래 버튼을 클릭하여 경품을 수령하세요
// ❌ 취약한 코드 - 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";
}
// ✅ 안전한 코드 - 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"
);
}
// 계속 처리...
}