CSRF(Cross-Site Request Forgery)는 사용자가 의도하지 않은 작업을 웹 애플리케이션에서 수행하도록 유도하는 공격 기법입니다. 이 공격은 사용자의 신원을 직접적으로 훔치지 않지만, 사용자가 자신도 모르는 사이에 행동을 취하도록 합니다. 이를 통해 공격자는 사용자의 권한을 이용해 악의적인 요청을 수행할 수 있습니다.
단계별 CSRF 공격 설명
단계 1: 사용자가 Netflix에 로그인
- 사용자는 netflix.com에 로그인합니다.
- Netflix 서버는 사용자의 인증을 확인한 후, 해당 사용자의 브라우저에 쿠키를 생성하고 저장합니다.
- 이 쿠키는 netflix.com 도메인에 대해 유효하며, 다른 도메인에서는 접근할 수 없습니다.
단계 2: 사용자가 악성 웹사이트(evil.com)에 방문
- 영화를 본 후, 사용자는 evil.com이라는 웹사이트를 방문합니다.
- 이 사이트는 해커가 운영하며, 사용자가 흥미를 가질 만한 콘텐츠(예: "아이폰 90% 할인")를 제공합니다.
- 사용자는 같은 브라우저의 다른 탭에서 evil.com에 접속합니다.
단계 3: 사용자가 악성 링크를 클릭
- 사용자가 evil.com에서 제공하는 링크를 클릭하면, 그 뒤에 숨겨진 악성 코드가 실행됩니다.
- 이 링크는 Netflix 서버에 이메일 변경 요청을 보내도록 설계되었습니다.
- 브라우저는 이 요청을 netflix.com으로 보내며, 함께 저장된 쿠키도 전송합니다.
- Netflix 서버는 요청이 실제 netflix.com에서 온 것인지, 악성 사이트에서 온 것인지 구분하지 못합니다.
- 결과적으로 사용자의 이메일이 해커가 지정한 이메일로 변경됩니다.
시나리오 예시
- 로그인 및 쿠키 생성:
- netflix.com에 로그인 시도
- Netflix 서버가 사용자 브라우저에 쿠키를 저장
- 악성 웹사이트 방문:
- 사용자가 evil.com에 접속
- 해커는 사용자가 링크를 클릭하도록 유도
- 악성 링크 클릭 및 이메일 변경 요청:
- 사용자가 링크를 클릭하여 악성 코드를 실행
- 브라우저가 netflix.com에 POST 요청을 보내고, 쿠키를 함께 전송
- Netflix 서버가 요청을 처리하여 사용자의 이메일을 변경
방어 방법
- CSRF 토큰 사용: 각 요청에 고유한 토큰을 포함시켜 요청의 유효성을 검증합니다.
- SameSite 쿠키 속성 사용: 쿠키가 동일한 사이트 내에서만 전송되도록 제한합니다.
- Referrer 검증: 요청의 출처를 검증하여 신뢰할 수 있는 도메인에서 온 요청만 처리합니다.
CSRF 공격은 사용자가 자신도 모르는 사이에 악의적인 요청을 수행하게 만들기 때문에 매우 위험합니다. 이를 방지하기 위해서는 적절한 보안 조치를 취하는 것이 중요합니다.
설정 방법
package com.example.loans_domain.config;
import com.example.loans_domain.filter.CsrfCookieFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.Collections;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// CSRF 토큰을 요청 속성에 설정하는 핸들러 생성
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName("_csrf");
http
.securityContext((context) -> context.requireExplicitSave(false))
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
.cors((cors) -> cors.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(Collections.singletonList("*"));
config.setMaxAge(3600L);
return config;
}
}))
.csrf((csrf) -> csrf
// CSRF 토큰 요청 핸들러 설정
.csrfTokenRequestHandler(requestHandler)
// /register 경로에 대해 CSRF 보호 비활성화
.ignoringRequestMatchers("/register")
// CSRF 토큰을 쿠키로 저장, HttpOnly 설정 비활성화
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
// CSRF 쿠키 필터를 BasicAuthenticationFilter 이후에 추가
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards").authenticated()
.requestMatchers("/notices", "/contact", "/h2-console/**", "/register").permitAll()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
package com.example.loans_domain.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 요청 속성에서 CSRF 토큰을 가져옴
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrfToken != null && csrfToken.getHeaderName() != null) {
// CSRF 토큰을 응답 헤더에 설정
response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
}
// 다음 필터로 요청과 응답을 전달
filterChain.doFilter(request, response);
}
}
Spring Security에서 CSRF 보호를 비활성화 (csrf().disable()) 하는 것은 보안상 매우 위험할 수 있습니다. 이는 애플리케이션이 CSRF(Cross-Site Request Forgery) 공격에 취약해질 수 있기 때문입니다.
CSRF 보호 비활성화:
- csrf.ignoringRequestMatchers("/register")는 /register 경로에 대해 CSRF 보호를 적용하지 않도록 설정합니다.
- 이는 해당 경로에 대한 요청이 CSRF 토큰 없이도 처리될 수 있음을 의미합니다.
왜 특정 경로에서 CSRF를 비활성화하는가?
특정 상황에서 CSRF 보호를 비활성화하는 것이 필요할 수 있습니다. 예를 들어, 다음과 같은 경우에 해당할 수 있습니다:
- 외부 시스템과의 통합: 외부 시스템이 CSRF 토큰을 포함할 수 없는 경우, 특정 엔드포인트에 대해 CSRF 보호를 비활성화해야 할 수 있습니다.
- 공개 API: 특정 공개 엔드포인트는 CSRF 보호가 필요하지 않을 수 있습니다.
현재 설정에서는 CSRF 토큰을 쿠키와 헤더 모두에 저장 및 전달하도록 설정되어 있습니다. 이 방식은 클라이언트 측에서 CSRF 토큰을 쉽게 접근하고 사용할 수 있게 합니다.
설명:
- **CookieCsrfTokenRepository**를 사용하여 CSRF 토큰을 쿠키에 저장:
- CookieCsrfTokenRepository는 CSRF 토큰을 쿠키로 저장하여, 클라이언트 측에서 접근할 수 있도록 합니다.
- withHttpOnlyFalse() 설정을 통해 JavaScript에서도 쿠키에 접근할 수 있도록 합니다.
- **CsrfCookieFilter**를 사용하여 CSRF 토큰을 응답 헤더에 설정:
- CsrfCookieFilter는 요청 속성에서 CSRF 토큰을 가져와 응답 헤더에 설정합니다.
- 이를 통해 클라이언트는 응답 헤더에서 CSRF 토큰을 읽어올 수 있습니다.
securityContext 설정
http.securityContext((context) -> context.requireExplicitSave(false))
설명:
- securityContext: Spring Security의 SecurityContext는 현재 인증된 사용자에 대한 보안 정보를 저장하는 컨테이너입니다.
- requireExplicitSave(false):
- 기본적으로 Spring Security는 보안 컨텍스트를 명시적으로 저장하지 않아도 자동으로 저장합니다.
- requireExplicitSave(false)는 보안 컨텍스트를 자동으로 저장하도록 설정하는 것입니다.
- 이는 보안 컨텍스트가 변경되면, 자동으로 현재 HTTP 세션에 보안 컨텍스트가 저장됩니다.
예시:
- 사용자가 애플리케이션에 로그인하면, Spring Security는 사용자의 인증 정보를 SecurityContext에 저장합니다. 이 설정은 이러한 보안 컨텍스트가 자동으로 세션에 저장되도록 합니다.
sessionManagement 설정
http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
설명:
- sessionManagement: 이 설정은 애플리케이션의 세션 관리 정책을 정의합니다.
- SessionCreationPolicy.ALWAYS:
- Spring Security는 요청이 들어올 때마다 새로운 HTTP 세션을 생성합니다.
- 이는 세션이 항상 생성되며, 세션이 없으면 새로운 세션을 만듭니다.
SessionCreationPolicy의 다른 옵션:
- ALWAYS: 항상 세션을 생성합니다.
- IF_REQUIRED: 필요할 때만 세션을 생성합니다. (기본 설정)
- NEVER: Spring Security는 세션을 생성하지 않지만, 이미 존재하는 세션을 사용할 수는 있습니다.
- STATELESS: Spring Security는 세션을 생성하거나 사용하지 않습니다. 주로 REST API와 같은 상태 비저장 애플리케이션에서 사용됩니다.
예시:
- 사용자가 애플리케이션을 방문할 때마다 새로운 세션이 생성됩니다. 이는 보안 강화 측면에서 세션 고정 공격(session fixation attack) 등을 방지하는 데 유용할 수 있습니다.
클라이언트 측에서 CSRF 토큰 사용
클라이언트 측에서는 이 토큰을 추출하여 다음 요청에 포함시킬 수 있습니다. 예를 들어, Vue.js 또는 React와 같은 프레임워크에서 Axios를 사용하여 요청을 보낼 때 다음과 같이 할 수 있습니다:
import axios from 'axios';
// CSRF 토큰을 쿠키에서 추출
const csrfToken = document.cookie.split('; ')
.find(row => row.startsWith('XSRF-TOKEN'))
.split('=')[1];
// Axios 기본 헤더에 CSRF 토큰을 설정
axios.defaults.headers.common['X-XSRF-TOKEN'] = csrfToken;
// 예시 요청
axios.post('/some-protected-endpoint', { data: 'example' })
.then(response => {
console.log('Response:', response);
})
.catch(error => {
console.error('Error:', error);
});