IT/CS 공부

[CS] CSRF(Cross-Site Request Forgery) 공격

박소민 2025. 6. 17. 12:03
CSRF 공격
사용자가 로그인된 상태에서 공격자가 유도한 악성 요청을 브라우저가 자동으로 실행해 서버에 전달하는 공격.
사용자는 자신도 모르게 중요한 요청(비밀번호 변경, 송금 등)을 실행하게 됨.

 

 

예시
  • 사용자가 securebank.com에 로그인하고 쿠키에 세션이 저장됨.
Cookie: JSESSIONID=abc123
  • 공격자가 만든 사이트 evil.com에 아래 코드가 있다면
    • 사용자가 evil.com 접속 → 브라우저가 쿠키 포함 요청 자동 전송 → 공격 성공.
<img src="https://securebank.com/transfer?amount=10000&to=999999" />

 

 

Spring Security를 활용한 CSRF 방어
  • Spring Security는 CSRF 방어 기능이 기본 활성화됨.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().enable()
            .authorizeRequests()
                .antMatchers("/transfer").authenticated()
                .anyRequest().permitAll();
    }
}

 

  • 폼에서 CSRF 토큰 사용하기
    • Spring은 서버 세션에 CSRF 토큰을 생성하고, 폼에 자동 삽입 가능.
# Thymeleaf 예시
<form action="/transfer" method="post">
    <input type="text" name="toAccount" />
    <input type="number" name="amount" />
    <input type="submit" value="Transfer" />
    
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
</form>
  • REST API에서 CSRF 토큰 사용
    • SPA, AJAX 환경에서 CSRF 토큰을 헤더로 전송해야 함
      • REST API + SPA 환경에서 CSRF 토큰을 사용하는 방식은 서버와 클라이언트가 분리되어 있고,
      • 폼(form) 제출이 아닌 JavaScript를 통한 AJAX 호출이기 때문에, CSRF 토큰을 헤더에 담아서 전송해야 함
        • JavaScript (fetch, axios 등) 기반 요청에서는 직접 토큰을 가져와서 헤더에 담아야 서버가 검증할 수 있음.
    • Spring Security는 이를 고려해 CSRF 토큰을 클라이언트가 쉽게 사용할 수 있도록 HTML 메타태그 또는 쿠키로 제공
흐름 요약

1. 서버가 최초 HTML을 렌더링할 때 CSRF 토큰 정보를 함께 내려줌
2. 클라이언트는 <meta> 태그나 쿠키에서 CSRF 토큰을 읽음
3. AJAX 요청 시 해당 토큰을 헤더에 추가하여 전송
4. 서버는 요청에 포함된 토큰과 세션의 토큰이 일치하는지 검증
# HTML
<meta name="_csrf" content="${_csrf.token}" />
<meta name="_csrf_header" content="${_csrf.headerName}" />

# 기본적으로 CSRF 토큰은 활성화돼 있으며, 
# HttpSessionCsrfTokenRepository를 사용하면 HTML 메타태그를 통해 내려보낼 수 있음.

#Java
@Bean
public CsrfTokenRepository csrfTokenRepository() {
    CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
    repository.setCookieName("XSRF-TOKEN");
    repository.setHeaderName("X-XSRF-TOKEN");
    return repository;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf()
        .csrfTokenRepository(csrfTokenRepository());
}
# Javascript
const token = document.querySelector('meta[name="_csrf"]').content;
const header = document.querySelector('meta[name="_csrf_header"]').content;

fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    [header]: token
  },
  body: JSON.stringify({ toAccount: '999999', amount: 10000 })
});
  • SameSite 쿠키 설정
    • Spring Boot에서 SameSite=Strict 설정 시, 크로스 도메인 요청에 쿠키 전송 차단됨.
server.servlet.session.cookie.same-site=strict